From bf688bf7f803581dfe1d41ec190ecba52aad6444 Mon Sep 17 00:00:00 2001 From: Arnie Date: Mon, 3 Nov 2025 22:13:37 +0100 Subject: [PATCH] Initial --- .gitignore | 10 + .npmrc | 2 + README.md | 37 + components/actions.c4c | 1 + components/commands.c4c | 1 + components/connections.c4c | 16 + components/events.c4c | 12 + components/navdisplayoptions.c4c | 1 + components/properties.c4c | 12 + components/proxies.c4c | 7 + components/ui.c4c | 1 + flake.lock | 276 ++++++ flake.nix | 84 ++ package-lock.json | 64 ++ package.json | 39 + src/control4-utils/base.lua | 1 + src/control4-utils/helpers.lua | 3 + src/control4-utils/helpers/all.lua | 6 + .../helpers/context/Context.lua | 36 + src/control4-utils/helpers/context/all.lua | 3 + src/control4-utils/helpers/logging/Logger.lua | 245 +++++ .../helpers/logging/RemoteLogger.lua | 339 +++++++ src/control4-utils/helpers/logging/all.lua | 4 + src/control4-utils/helpers/strings/Trim.lua | 7 + src/control4-utils/helpers/strings/all.lua | 3 + .../helpers/timers/OneShotTimer.lua | 43 + src/control4-utils/helpers/timers/all.lua | 3 + src/control4-utils/hooks.lua | 3 + src/control4-utils/hooks/ExecuteCommand.lua | 30 + src/control4-utils/hooks/OnDriverLateInit.lua | 28 + .../hooks/OnPropertyChanged.lua | 29 + .../hooks/ReceivedFromProxy.lua | 31 + src/control4-utils/hooks/all.lua | 6 + src/control4-utils/knx.lua | 3 + src/control4-utils/knx/DPT.lua | 25 + src/control4-utils/knx/GroupAddress.lua | 235 +++++ src/control4-utils/knx/Proxy.lua | 68 ++ src/control4-utils/knx/all.lua | 7 + src/control4-utils/knx/gen/ADDRESSES.lua | 927 ++++++++++++++++++ .../knx/gen/createGroupAddresses.lua | 628 ++++++++++++ src/driver.lua | 52 + src/lib/createContext.lua | 14 + src/lib/presence/LF/Hallway.lua | 27 + src/lib/presence/LF/index.lua | 1 + src/lib/presence/UF/Bathroom.lua | 191 ++++ src/lib/presence/UF/index.lua | 1 + src/lib/presence/index.lua | 2 + src/lib/state/daytime.lua | 13 + src/lib/state/index.lua | 2 + src/www/documentation.html | 15 + src/www/icons/logo.png | Bin 0 -> 88318 bytes src/www/icons/logo_16.png | Bin 0 -> 1335 bytes src/www/icons/logo_32.png | Bin 0 -> 2702 bytes tests/test.lua | 24 + 54 files changed, 3618 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 components/actions.c4c create mode 100644 components/commands.c4c create mode 100644 components/connections.c4c create mode 100644 components/events.c4c create mode 100644 components/navdisplayoptions.c4c create mode 100644 components/properties.c4c create mode 100644 components/proxies.c4c create mode 100644 components/ui.c4c create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/control4-utils/base.lua create mode 100644 src/control4-utils/helpers.lua create mode 100644 src/control4-utils/helpers/all.lua create mode 100644 src/control4-utils/helpers/context/Context.lua create mode 100644 src/control4-utils/helpers/context/all.lua create mode 100644 src/control4-utils/helpers/logging/Logger.lua create mode 100644 src/control4-utils/helpers/logging/RemoteLogger.lua create mode 100644 src/control4-utils/helpers/logging/all.lua create mode 100644 src/control4-utils/helpers/strings/Trim.lua create mode 100644 src/control4-utils/helpers/strings/all.lua create mode 100644 src/control4-utils/helpers/timers/OneShotTimer.lua create mode 100644 src/control4-utils/helpers/timers/all.lua create mode 100644 src/control4-utils/hooks.lua create mode 100644 src/control4-utils/hooks/ExecuteCommand.lua create mode 100644 src/control4-utils/hooks/OnDriverLateInit.lua create mode 100644 src/control4-utils/hooks/OnPropertyChanged.lua create mode 100644 src/control4-utils/hooks/ReceivedFromProxy.lua create mode 100644 src/control4-utils/hooks/all.lua create mode 100644 src/control4-utils/knx.lua create mode 100644 src/control4-utils/knx/DPT.lua create mode 100644 src/control4-utils/knx/GroupAddress.lua create mode 100644 src/control4-utils/knx/Proxy.lua create mode 100644 src/control4-utils/knx/all.lua create mode 100644 src/control4-utils/knx/gen/ADDRESSES.lua create mode 100644 src/control4-utils/knx/gen/createGroupAddresses.lua create mode 100644 src/driver.lua create mode 100644 src/lib/createContext.lua create mode 100644 src/lib/presence/LF/Hallway.lua create mode 100644 src/lib/presence/LF/index.lua create mode 100644 src/lib/presence/UF/Bathroom.lua create mode 100644 src/lib/presence/UF/index.lua create mode 100644 src/lib/presence/index.lua create mode 100644 src/lib/state/daytime.lua create mode 100644 src/lib/state/index.lua create mode 100644 src/www/documentation.html create mode 100644 src/www/icons/logo.png create mode 100644 src/www/icons/logo_16.png create mode 100644 src/www/icons/logo_32.png create mode 100644 tests/test.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94fad8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/log/ +/intermediate/ +/node_modules/ +/output/ +/environment.json + +.direnv +.devenv +.vscode +.envrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..64f9127 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@annex4:registry=https://npm.pkg.github.com +@c3c:registry=https://git.c3c.cz/api/packages/c3c/npm/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7010887 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Control4 KNX Lights + +Control4 driver managing the lights system + + +Notes: + + +Audio + - any manual action = override ON + +Lights + - any manual action = override ON + +Presence detected + - turn on lights + +Presence detected +10s + - turn on audio + +Presence detected +3 min + - Ventilation intake OFF + - Ventilation out 50% + + +Presence ended + - turn off lights if not overridden + - turn off audio if not overridden + +Presence ended + 2 minutes + - turn off lights + - if humidity > 80%, Ventilation intake normal, Ventilation out 100% + - watch humidity on interval, when humidity < 80%, ventilation normal + +Presence ended + 5 minutes + - turn off audio if playing +F diff --git a/components/actions.c4c b/components/actions.c4c new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/components/actions.c4c @@ -0,0 +1 @@ +[] diff --git a/components/commands.c4c b/components/commands.c4c new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/components/commands.c4c @@ -0,0 +1 @@ +[] diff --git a/components/connections.c4c b/components/connections.c4c new file mode 100644 index 0000000..bbcdb29 --- /dev/null +++ b/components/connections.c4c @@ -0,0 +1,16 @@ +[ + { + "id": 1, + "connectionname": "KNX Control", + "type": 1, + "consumer": true, + "classes": [ + { + "classname": "KNX_DEVICE", + "autobind": false + } + ], + "linelevel": true, + "facing": 6 + } +] diff --git a/components/events.c4c b/components/events.c4c new file mode 100644 index 0000000..2b71cb1 --- /dev/null +++ b/components/events.c4c @@ -0,0 +1,12 @@ +[ + { + "id": 100, + "description": "Start Media in Upper Floor Bathroom", + "name": "UpperFloorBathroomMediaStart" + }, + { + "id": 101, + "description": "End Media in Upper Floor Bathroom", + "name": "UpperFloorBathroomMediaEnd" + } +] diff --git a/components/navdisplayoptions.c4c b/components/navdisplayoptions.c4c new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/components/navdisplayoptions.c4c @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/components/properties.c4c b/components/properties.c4c new file mode 100644 index 0000000..1f11c23 --- /dev/null +++ b/components/properties.c4c @@ -0,0 +1,12 @@ +[ + { + "name": "Log Level", + "type": "DYNAMIC_LIST", + "readonly": false + }, + { + "name": "Log Mode", + "type": "DYNAMIC_LIST", + "readonly": false + } +] diff --git a/components/proxies.c4c b/components/proxies.c4c new file mode 100644 index 0000000..e60129f --- /dev/null +++ b/components/proxies.c4c @@ -0,0 +1,7 @@ +[ + { + "id": 5000, + "proxy": "c3c-knx-presence", + "name": "KNX Presence" + } +] diff --git a/components/ui.c4c b/components/ui.c4c new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/components/ui.c4c @@ -0,0 +1 @@ +[] diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7097d60 --- /dev/null +++ b/flake.lock @@ -0,0 +1,276 @@ +{ + "nodes": { + "cachix": { + "inputs": { + "devenv": [ + "nix", + "devenv" + ], + "flake-compat": [ + "nix", + "devenv" + ], + "git-hooks": [ + "nix", + "devenv" + ], + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1742042642, + "narHash": "sha256-D0gP8srrX0qj+wNYNPdtVJsQuFzIng3q43thnHXQ/es=", + "owner": "cachix", + "repo": "cachix", + "rev": "a624d3eaf4b1d225f918de8543ed739f2f574203", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "devenv": { + "inputs": { + "cachix": "cachix", + "flake-compat": "flake-compat", + "git-hooks": "git-hooks", + "nix": "nix_2", + "nixpkgs": [ + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1744876578, + "narHash": "sha256-8MTBj2REB8t29sIBLpxbR0+AEGJ7f+RkzZPAGsFd40c=", + "owner": "cachix", + "repo": "devenv", + "rev": "7ff7c351bba20d0615be25ecdcbcf79b57b85fe1", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nix", + "devenv", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "nix", + "devenv" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "nix", + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1742649964, + "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "nix", + "devenv", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "libgit2": { + "flake": false, + "locked": { + "lastModified": 1697646580, + "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=", + "owner": "libgit2", + "repo": "libgit2", + "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5", + "type": "github" + }, + "original": { + "owner": "libgit2", + "repo": "libgit2", + "type": "github" + } + }, + "nix": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1744882253, + "narHash": "sha256-SHedYfKXqJ0BuB0XDaIbWS0umcqj9ScYTrrWYN8HmMo=", + "ref": "refs/heads/main", + "rev": "49a4dc963156c87f2c13d0ef3db0d8d244bdaa2d", + "revCount": 14, + "type": "git", + "url": "ssh://git@git.c3c.cz/C3C/nix" + }, + "original": { + "type": "git", + "url": "ssh://git@git.c3c.cz/C3C/nix" + } + }, + "nix_2": { + "inputs": { + "flake-compat": [ + "nix", + "devenv" + ], + "flake-parts": "flake-parts", + "libgit2": "libgit2", + "nixpkgs": "nixpkgs_2", + "nixpkgs-23-11": [ + "nix", + "devenv" + ], + "nixpkgs-regression": [ + "nix", + "devenv" + ], + "pre-commit-hooks": [ + "nix", + "devenv" + ] + }, + "locked": { + "lastModified": 1741798497, + "narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=", + "owner": "domenkozar", + "repo": "nix", + "rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd", + "type": "github" + }, + "original": { + "owner": "domenkozar", + "ref": "devenv-2.24", + "repo": "nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1733212471, + "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1717432640, + "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "88269ab3044128b7c2f4c7d68448b2fb50456870", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nix": "nix" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0267978 --- /dev/null +++ b/flake.nix @@ -0,0 +1,84 @@ +{ + inputs = { + nix.url = "git+ssh://git@git.c3c.cz/C3C/nix"; + }; + + outputs = + { self, nix }: + { + formatter = nix.formatter; + + packages = nix.lib.forAllSystems (pkgs: { + devenv-up = self.devShells.${pkgs.system}.default.config.procfileScript; + devenv-test = self.devShells.${pkgs.system}.default.config.test; + }); + + devShells = nix.lib.forAllSystems (pkgs: { + default = nix.lib.mkDevenvShell { + inherit pkgs; + + inputs = { + self = self; + nixpkgs = pkgs; + }; + + modules = [ + { + packages = [ + nix.lib.control4-env.${pkgs.system} + ]; + + scripts = { + menu = { + description = "Print this menu"; + exec = '' + echo "Commands:" + echo -n '${ + builtins.toJSON ( + builtins.mapAttrs (s: value: value.description) self.devShells.${pkgs.system}.default.config.scripts + ) + }' | \ + ${pkgs.jq}/bin/jq -r 'to_entries | map(" \(.key)\n" + " - \(if .value == "" then "no description provided" else .value end)") | "" + .[]' + ''; + }; + + generate = { + exec = '' + ${nix.lib.cd_root} + + rm -rf ./src/control4-utils + cp -rp node_modules/@c3c/control4-utils/src ./src/control4-utils + node_modules/@c3c/control4-utils/bin/driver-manager ./package.json + mdtohtml -page README.md > src/www/documentation.html + ''; + }; + + github-push = { + description = "Push current branch and all tags to github, lazy workaround for automated mirror. TODO: Automate"; + exec = '' + b=$(git rev-parse --abbrev-ref HEAD) + git push git@github.com:Sharsie/control4-knx-presence.git "$b":"$b" + git push git@github.com:Sharsie/control4-knx-presence.git --tags + ''; + }; + + lint = { + exec = '' + ${nix.lib.cd_root} + ''; + }; + + fix = { + exec = '' + ${nix.lib.cd_root} + nix fmt ./*.nix + stylua ./src + ''; + }; + }; + } + ]; + }; + }); + }; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8349026 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,64 @@ +{ + "name": "c3c-knx-presence", + "version": "0.0.223", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "c3c-knx-presence", + "version": "0.0.223", + "hasInstallScript": true, + "license": "GPL-3.0-only", + "devDependencies": { + "@c3c/control4-utils": "^0.5.2", + "@prettier/plugin-lua": "0.0.2", + "prettier": "^3" + } + }, + "node_modules/@c3c/control4-utils": { + "version": "0.5.2", + "resolved": "https://git.c3c.cz/api/packages/C3C/npm/%40c3c%2Fcontrol4-utils/-/0.5.2/control4-utils-0.5.2.tgz", + "integrity": "sha512-aKbyjB7SG8mZxYhuBnqngWA1RaFL+3TsH+CDtkOzb4VGZ9VfLk/3a4uvREuClSJMqC6EdrG1t9gz5DTwL2h1Uw==", + "dev": true, + "license": "GPL-3.0-only" + }, + "node_modules/@prettier/plugin-lua": { + "version": "0.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "luaparse": "0.2.1" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "prettier": ">=2.0.0" + } + }, + "node_modules/luaparse": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "bin": { + "luaparse": "bin/luaparse" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..81fd0a4 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "author": "C3C", + "control4": { + "capabilities": {}, + "control": "lua_gen", + "controlmethod": "KNX", + "created": "5/14/2023, 10:16:49 PM", + "icon": { + "image_source": "c4z", + "large": "icons/logo_32.png", + "small": "icons/logo_16.png" + }, + "manufacturer": "C3C", + "model": "Presence", + "name": "KNX Presence" + }, + "description": "", + "devDependencies": { + "@c3c/control4-utils": "^0.5.2", + "@prettier/plugin-lua": "0.0.2", + "prettier": "^3" + }, + "homepage": "https://git.c3c.cz/C3C/control4-knx-presence", + "keywords": [], + "license": "GPL-3.0-only", + "main": "src/driver.lua", + "name": "c3c-knx-presence", + "repository": { + "type": "git", + "url": "https://git.c3c.cz/C3C/control4-knx-presence.git" + }, + "scripts": { + "driver-manager": "node_modules/@c3c/control4-utils/bin/driver-manager ./package.json", + "install-control4-utils": "rm -rf ./src/control4-utils && cp -rp node_modules/@c3c/control4-utils/src ./src/control4-utils", + "postinstall": "npm run install-control4-utils && npm run driver-manager", + "test": "busted tests/test.lua" + }, + "version": "0.0.223" +} diff --git a/src/control4-utils/base.lua b/src/control4-utils/base.lua new file mode 100644 index 0000000..221e77f --- /dev/null +++ b/src/control4-utils/base.lua @@ -0,0 +1 @@ +C3C = {} diff --git a/src/control4-utils/helpers.lua b/src/control4-utils/helpers.lua new file mode 100644 index 0000000..23463be --- /dev/null +++ b/src/control4-utils/helpers.lua @@ -0,0 +1,3 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "helpers.all") diff --git a/src/control4-utils/helpers/all.lua b/src/control4-utils/helpers/all.lua new file mode 100644 index 0000000..2f1397f --- /dev/null +++ b/src/control4-utils/helpers/all.lua @@ -0,0 +1,6 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "context.all") +require(dirRequire .. "logging.all") +require(dirRequire .. "strings.all") +require(dirRequire .. "timers.all") diff --git a/src/control4-utils/helpers/context/Context.lua b/src/control4-utils/helpers/context/Context.lua new file mode 100644 index 0000000..7e5adbe --- /dev/null +++ b/src/control4-utils/helpers/context/Context.lua @@ -0,0 +1,36 @@ +---@alias C3CContext table +do + if not C3C then + print("Control4Utils: ERROR LOADING src.helpers.context.Context, src.base must be required first") + return + end + + C3C.Context = { + ---@param parentCtx C3CContext + ---@param stack string? + ---@return C3CContext + Attach = function(parentCtx, stack) + local ctx = {} + for k, v in pairs(parentCtx) do + if type(k) == "string" then + local vt = type(v) + if vt == "string" or vt == "number" or vt == "boolean" then + ctx[k] = v + end + end + end + + if not ctx.start then + ctx.start = C4:GetTime() + end + + if not ctx.stack then + ctx.stack = "" + elseif stack and stack ~= "" and type(ctx.stack) == "string" then + ctx.stack = ctx.stack .. " -> " .. stack + end + + return ctx + end, + } +end diff --git a/src/control4-utils/helpers/context/all.lua b/src/control4-utils/helpers/context/all.lua new file mode 100644 index 0000000..c78213a --- /dev/null +++ b/src/control4-utils/helpers/context/all.lua @@ -0,0 +1,3 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "Context") diff --git a/src/control4-utils/helpers/logging/Logger.lua b/src/control4-utils/helpers/logging/Logger.lua new file mode 100644 index 0000000..455eb59 --- /dev/null +++ b/src/control4-utils/helpers/logging/Logger.lua @@ -0,0 +1,245 @@ +---@alias C3CLogFN fun(s: string): nil + +do + if not C3C then + print("Control4Utils: ERROR LOADING src.helpers.logging.Logger, src/base.lua must be required first") + return + end + + local logLevelPropName = "Log Level" + local logModePropName = "Log Mode" + local logLevels = "" + local logModes = "" + local defaultLogLevel = "Error" + local defaultLogMode = "Off" + + local disableRemotelogging = false + + local availableLevels = { + ["Debug"] = "debug", + ["Error"] = "error", + ["Info"] = "info", + } + + for k, _ in pairs(availableLevels) do + logLevels = logLevels .. k .. "," + end + + logLevels = logLevels:sub(1, -2) + + local availableModes = { + ["Print"] = "print", + ["Log"] = "log", + ["Print and Log"] = "printlog", + ["Off"] = "off", + } + + for k, _ in pairs(availableModes) do + logModes = logModes .. k .. "," + end + + logModes = logModes:sub(1, -2) + + -- The following functions are helpers for disabling + -- and enabling logging based on mode and log level + local noop = function(s) end + + -- Log function is set using the logging mode, to either print + -- or error log into the director + local logFn = noop + + -- Print function is set when log leve changes, it internally uses the logFn set by logging mode + local printFn = function(s) + logFn(s) + end + + ---@type {debug: C3CLogFN, error: C3CLogFN, info: C3CLogFN} + local log = { + debug = noop, + error = noop, + info = noop, + } + + --- Sets the logging verbosity + ---@param level "Debug"|"Info"|"Error" + local function setLogLevel(level) + if availableLevels[level] == nil then + print(string.format("Invalid logging level requested: %s", level)) + return + end + + print("Setting log level:" .. level) + if level == "Debug" then + log.debug = printFn + log.info = printFn + log.error = printFn + elseif level == "Error" then + log.debug = noop + log.info = noop + log.error = printFn + elseif level == "Info" then + log.debug = noop + log.info = printFn + log.error = printFn + end + end + + --- Sets the target where logging will be sent + ---@param mode "Print"|"Log"|"Print and Log"|"Off" + local function setLoggingMode(mode) + if availableModes[mode] == nil then + print(string.format("Invalid logging mode requested: %s", mode)) + return + end + + print("Setting log mode:" .. mode) + if mode == "Off" then + logFn = noop + elseif mode == "Print" then + logFn = function(s) + print(s) + end + elseif mode == "Log" then + logFn = function(s) + C4:ErrorLog(s) + end + elseif mode == "Print and Log" then + logFn = function(s) + print(s) + C4:ErrorLog(s) + end + end + end + + ---Formats a table into a string + ---@param tParams table + ---@return string + local function formatParams(tParams) + tParams = tParams or {} + local out = {} + for k, v in pairs(tParams) do + if type(v) == "string" then + local vString = tostring(v) + if vString:len() > 1000 then + table.insert(out, k .. ': "' .. vString:sub(1, 1000) .. ' [...]"') + else + table.insert(out, k .. ': "' .. vString .. '"') + end + elseif type(v) == "table" then + table.insert(out, k .. ": [" .. formatParams(v) .. "]") + else + table.insert(out, k .. ": " .. tostring(v):sub(1, 100)) + end + end + return table.concat(out, ", ") + end + + if C3C.HookIntoOnDriverLateInit then + C3C.HookIntoOnDriverLateInit(function() + local logLevel = Properties[logLevelPropName] + local logMode = Properties[logModePropName] + + if not logLevel or logLevel == "" then + logLevel = defaultLogLevel + end + if not logMode or logMode == "" then + logMode = defaultLogMode + end + + C4:UpdatePropertyList(logLevelPropName, logLevels, logLevel) + C4:UpdatePropertyList(logModePropName, logModes, logMode) + + setLogLevel(logLevel) + setLoggingMode(logMode) + end) + else + print("ERROR: HookIntoOnDriverLateInit is not loaded") + end + + if C3C.HookIntoOnPropertyChanged then + C3C.HookIntoOnPropertyChanged(function(strProperty) + if strProperty == logLevelPropName then + setLogLevel(Properties[logLevelPropName]) + elseif strProperty == logModePropName then + setLoggingMode(Properties[logModePropName]) + end + end) + else + print("ERROR: HookIntoOnPropertyChanged is not loaded") + end + + C3C.Logger = { + DisableRemoteLogging = function() + disableRemotelogging = true + end, + + ---Sends the message to a debug channel + ---@param s string + ---@param ctx table|nil + Debug = function(s, ctx) + local disableRemote = disableRemotelogging + if ctx and ctx.disableRemoteLog == true then + disableRemote = true + ctx.disableRemoteLog = nil + end + + if ctx ~= nil then + log.debug("[DEBUG] > " .. s .. " | CTX: " .. formatParams(ctx)) + else + log.debug("[DEBUG] > " .. s) + end + + if not disableRemote and C3C.RemoteLogger then + local remoteCtx = ctx or {} + remoteCtx.source = "Logger" + C3C.RemoteLogger.Debug(s, remoteCtx) + end + end, + + ---Sends the message to an error channel + ---@param s string + ---@param ctx table|nil + Error = function(s, ctx) + local disableRemote = disableRemotelogging + if ctx and ctx.disableRemoteLog == true then + disableRemote = true + ctx.disableRemoteLog = nil + end + + if ctx ~= nil then + log.error("[ERROR] > " .. s .. " | CTX: " .. formatParams(ctx)) + else + log.error("[ERROR] > " .. s) + end + + if not disableRemote and C3C.RemoteLogger then + local remoteCtx = ctx or {} + remoteCtx.source = "Logger" + C3C.RemoteLogger.Error(s, remoteCtx) + end + end, + + ---Sends the message to an info channel + ---@param s string + ---@param ctx table|nil + Info = function(s, ctx) + local disableRemote = disableRemotelogging + if ctx and ctx.disableRemoteLog == true then + disableRemote = true + ctx.disableRemoteLog = nil + end + + if ctx ~= nil then + log.info("[ INFO] > " .. s .. " | CTX: " .. formatParams(ctx)) + else + log.info("[ INFO] > " .. s) + end + + if not disableRemote and C3C.RemoteLogger then + local remoteCtx = ctx or {} + remoteCtx.source = "Logger" + C3C.RemoteLogger.Info(s, remoteCtx) + end + end, + } +end diff --git a/src/control4-utils/helpers/logging/RemoteLogger.lua b/src/control4-utils/helpers/logging/RemoteLogger.lua new file mode 100644 index 0000000..9611d18 --- /dev/null +++ b/src/control4-utils/helpers/logging/RemoteLogger.lua @@ -0,0 +1,339 @@ +---@alias C3CLogLevel +---|"DEBUG" +---|"INFO" +---|"WARN" +---|"ERROR" + +---@alias C3CRemoteLoggerPayload table + +--- time_of_execution in milliseconds, use C4:GetTime() +---@alias C3CLoggerPayload { level: C3CLogLevel, message: string, service_identifier: string, time_of_execution: number, [string]: string|number|boolean } +---@alias C3CMetricValue string|number|boolean +---@alias C3CMetricPayload { metric: string, service_identifier: string, time_of_execution: number, [string]: C3CMetricValue } + +---@alias C3CRemoteLogFN fun(message: string, context: C3CRemoteLoggerPayload): boolean +---@alias C3CMetricFN fun(metric: string, context: C3CRemoteLoggerPayload): boolean + +C3CRemoteLoggerCastKeyPrefix = "_cast_" +C3CRemoteLoggerTagKeyPrefix = "_tag_" +C3CRemoteLoggerFieldKeyPrefix = "_field_" + +do + if not C3C then + print("Control4Utils: ERROR LOADING src.helpers.logging.RemoteLogger, src/base.lua must be required first") + return + end + + local LoggerConnectionBindingID = 100 + + -- service identifier + ---@type string|nil + local SI = nil + + ---@param payload C3CRemoteLoggerPayload + ---@return table + local encodeTypedPayload = function(payload) + ---@type table + local output = {} + local errorCtx = { + level = "ERROR", + message = "Service tried to encode data for remote logger", + } + local error = false + + for k, v in pairs(payload) do + if k:sub(1, C3CRemoteLoggerCastKeyPrefix:len()) == C3CRemoteLoggerCastKeyPrefix then + error = true + errorCtx["offending_key_" .. k] = k + end + + local vType = type(v) + if vType ~= "nil" and vType ~= "number" and vType ~= "string" and vType ~= "boolean" then + output[k] = "invalid field value type " .. vType + else + output[k] = tostring(v) + if vType ~= "string" then + output[C3CRemoteLoggerCastKeyPrefix .. k] = vType + end + end + end + + if error then + C3C.Logger.Error("Tried to remote log data with reserved context keys", errorCtx) + end + + return output + end + + ---@param tags table + ---@param fields table + ---@return table + local encodeMetrics = function(tags, fields) + ---@type table + local output = {} + + for k, v in pairs(tags) do + output[C3CRemoteLoggerTagKeyPrefix .. k] = v + end + + for k, v in pairs(fields) do + output[C3CRemoteLoggerFieldKeyPrefix .. k] = v + end + + return output + end + + ---@param level C3CLogLevel + ---@param message string + ---@param ctx C3CRemoteLoggerPayload + ---@return boolean + local sendLog = function(level, message, ctx) + if SI == nil then + -- Make sure we do not loop + C3C.Logger.Error( + "Tried to remote log without setting up service identifier, call RemoteLogger.Setup first", + { + + -- Make sure we do not loop + disableRemoteLog = true, + } + ) + return false + end + + ---@type C3CLoggerPayload + local payload = { + level = "ERROR", + message = "", + service_identifier = SI, + time_of_execution = C4:GetTime(), + } + + if ctx.level ~= nil or ctx.message ~= nil or ctx.service_identifier ~= nil or ctx.time_of_execution ~= nil then + C3C.Logger.Error("Tried to remote log data with reserved context keys", { + + -- Make sure we do not loop + disableRemoteLog = true, + message = message, + level = level, + ctx = ctx, + }) + + payload.level = "ERROR" + payload.message = "Service tried to remote log, but used reserved keyword in the context value" + + C4:SendToProxy(LoggerConnectionBindingID, "INSERT_LOG", payload, "NOTIFY") + return false + end + + payload.level = level + payload.message = message + + for k, v in pairs(ctx) do + payload[k] = v + end + + C4:SendToProxy(LoggerConnectionBindingID, "INSERT_LOG", encodeTypedPayload(payload), "NOTIFY") + + return true + end + + ---@param metric string + ---@param tags table + ---@param fields table + ---@return boolean + local sendMetric = function(metric, tags, fields) + if SI == nil then + C3C.Logger.Error( + "tried to add remote metric without setting up service identifier, call C3C.RemoteLogger.Setup first", + { + disableRemoteLog = true, + stack = "RemoteLogger.sendMetric", + } + ) + return false + end + + ---@type C3CMetricPayload + local payload = { + metric = metric, + service_identifier = SI, + time_of_execution = C4:GetTime(), + } + + if tags.service_identifier ~= nil or tags.time_of_execution ~= nil then + C3C.Logger.Error( + "Tried to add remote metric data with reserved tag keywords", + { metric = metric, tags = tags } + ) + + payload.level = "ERROR" + payload.message = "Service tried to add remote metric, but used reserved tag keywords" + + C4:SendToProxy(LoggerConnectionBindingID, "INSERT_LOG", payload, "NOTIFY") + return false + end + + payload.metric = metric + + for k, v in pairs(encodeMetrics(tags, fields)) do + payload[k] = v + end + + C4:SendToProxy(LoggerConnectionBindingID, "INSERT_METRIC", encodeTypedPayload(payload), "NOTIFY") + + return true + end + + C3C.RemoteLogger = { + ---@param payload table + ---@return C3CRemoteLoggerPayload + DecodeUntypedPayload = function(payload) + ---@type C3CRemoteLoggerPayload + local output = {} + + for k, v in pairs(payload) do + if k:sub(1, C3CRemoteLoggerCastKeyPrefix:len()) ~= C3CRemoteLoggerCastKeyPrefix then + local castValue = payload[C3CRemoteLoggerCastKeyPrefix .. k] + if castValue == "" or castValue == nil then + -- performance + output[k] = v + elseif castValue == "number" then + local numVal = tonumber(v) + if type(numVal) == "number" then + output[k] = numVal + else + output[k] = v + end + elseif castValue == "boolean" then + if v:lower() == "true" or v == "1" then + output[k] = true + else + output[k] = false + end + else + output[k] = v + end + end + end + + return output + end, + + ---@param payload table + ---@return {fields: table, tags: table} + DecodeMetrics = function(payload) + ---@type {fields: table, tags: table} + local output = { + fields = {}, + tags = {}, + } + + for k, v in pairs(payload) do + if k:sub(1, C3CRemoteLoggerTagKeyPrefix:len()) == C3CRemoteLoggerTagKeyPrefix then + output.tags[k:sub(C3CRemoteLoggerTagKeyPrefix:len() + 1)] = v + elseif k:sub(1, C3CRemoteLoggerFieldKeyPrefix:len()) == C3CRemoteLoggerFieldKeyPrefix then + output.fields[k:sub(C3CRemoteLoggerFieldKeyPrefix:len() + 1)] = v + end + end + + return output + end, + + ---@param service_identifier string + Setup = function(service_identifier) + SI = service_identifier + C4:AddDynamicBinding( + LoggerConnectionBindingID, + "CONTROL", + false, + "Logger", + "c3c-remote-logger", + false, + true + ) + end, + + ---@type C3CRemoteLogFN + Debug = function(message, ctx) + return sendLog("DEBUG", message, ctx) + end, + + ---@type C3CRemoteLogFN + Info = function(message, ctx) + return sendLog("INFO", message, ctx) + end, + + ---@type C3CRemoteLogFN + Warn = function(message, ctx) + return sendLog("WARN", message, ctx) + end, + + ---@type C3CRemoteLogFN + Error = function(message, ctx) + return sendLog("ERROR", message, ctx) + end, + + -- Example fields: humidity, temperature, co2 + ---@param sensorId string + ---@param location string + ---@param fields table + ---@param additionalTags nil|table + AirSensorMetric = function(sensorId, location, fields, additionalTags) + ---@type table + local tags = { + sensor_id = sensorId, + location = location, + } + + if additionalTags ~= nil then + for k, v in pairs(additionalTags) do + tags[k] = v + end + end + + return sendMetric("air_sensor", tags, fields) + end, + + -- Example kinds: water_level, door_open + -- Example fields: below_threshold, open + ---@param sensorId string + ---@param kind string + ---@param fields table + ---@param additionalTags nil|table + StatusMetric = function(sensorId, kind, fields, additionalTags) + ---@type table + local tags = { + sensor_id = sensorId, + kind = kind, + } + + if additionalTags ~= nil then + for k, v in pairs(additionalTags) do + tags[k] = v + end + end + + return sendMetric("status", tags, fields) + end, + + -- Example fields: airflow, boost, setpoint + ---@param sensorId string + ---@param fields table + ---@param additionalTags nil|table + VentilationMetric = function(sensorId, fields, additionalTags) + ---@type table + local tags = { + sensor_id = sensorId, + } + + if additionalTags ~= nil then + for k, v in pairs(additionalTags) do + tags[k] = v + end + end + + return sendMetric("ventilation", tags, fields) + end, + } +end diff --git a/src/control4-utils/helpers/logging/all.lua b/src/control4-utils/helpers/logging/all.lua new file mode 100644 index 0000000..c868113 --- /dev/null +++ b/src/control4-utils/helpers/logging/all.lua @@ -0,0 +1,4 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "Logger") +require(dirRequire .. "RemoteLogger") diff --git a/src/control4-utils/helpers/strings/Trim.lua b/src/control4-utils/helpers/strings/Trim.lua new file mode 100644 index 0000000..ed89205 --- /dev/null +++ b/src/control4-utils/helpers/strings/Trim.lua @@ -0,0 +1,7 @@ +--- Trim whitespace at start and end of string +---@param s string +---@return string, number +function Trim(s) -- Source: PiL2 20.4 + s = s or "" + return s:gsub("^%s*(.-)%s*$", "%1") +end diff --git a/src/control4-utils/helpers/strings/all.lua b/src/control4-utils/helpers/strings/all.lua new file mode 100644 index 0000000..b150553 --- /dev/null +++ b/src/control4-utils/helpers/strings/all.lua @@ -0,0 +1,3 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "Trim") diff --git a/src/control4-utils/helpers/timers/OneShotTimer.lua b/src/control4-utils/helpers/timers/OneShotTimer.lua new file mode 100644 index 0000000..af7c842 --- /dev/null +++ b/src/control4-utils/helpers/timers/OneShotTimer.lua @@ -0,0 +1,43 @@ +do + if not C3C then + print("Control4Utils: ERROR LOADING src.helpers.timers.OneShotTimer, src/base.lua must be required first") + return + end + + ---@type table + local timers = {} + + C3C.OneShotTimer = { + ClearAll = function() + for _, timer in pairs(timers) do + timer:Cancel() + end + timers = {} + end, + + ---@param nDelay number Numeric value in milliseconds which is the desired timer delay. This value must be greater than 0. + ---@param fCallback fun(self: C4LuaTimer, skips: number) The function to be called when the timer fires. The function signature for non-repeating timers is: function(timer) + ---@param Name string? + Add = function(nDelay, fCallback, Name) + local timer = C4:SetTimer(nDelay, fCallback, false) + + -- Look for name if not nil, if found, remove existing timer callback... + if Name ~= nil then + for k, v in pairs(timers) do + if k == Name then + v:Cancel() + timers[k] = nil + end + end + + timers[Name] = timer + end + end, + } + + if C3C.HookIntoOnDriverLateInit then + C3C.HookIntoOnDriverLateInit(C3C.OneShotTimer.ClearAll) + else + print("hook HookIntoOnDriverLateInit must be loaded") + end +end diff --git a/src/control4-utils/helpers/timers/all.lua b/src/control4-utils/helpers/timers/all.lua new file mode 100644 index 0000000..e8c5c60 --- /dev/null +++ b/src/control4-utils/helpers/timers/all.lua @@ -0,0 +1,3 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "OneShotTimer") diff --git a/src/control4-utils/hooks.lua b/src/control4-utils/hooks.lua new file mode 100644 index 0000000..424fb7c --- /dev/null +++ b/src/control4-utils/hooks.lua @@ -0,0 +1,3 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "hooks.all") diff --git a/src/control4-utils/hooks/ExecuteCommand.lua b/src/control4-utils/hooks/ExecuteCommand.lua new file mode 100644 index 0000000..75ccfeb --- /dev/null +++ b/src/control4-utils/hooks/ExecuteCommand.lua @@ -0,0 +1,30 @@ +---@alias C3CExecuteCommandCallback fun(strCommand: string, tParams: table) +do + if not C3C then + print("Control4Utils: ERROR LOADING src.hooks.ExecuteCommand, src/base.lua must be required first") + return + end + + -- Stores global hook if already defined + local prevHook + + ---@type C3CExecuteCommandCallback[] + local hooks = {} + + ---@param callback C3CExecuteCommandCallback + C3C.HookIntoExecuteCommand = function(callback) + table.insert(hooks, callback) + end + + prevHook, ExecuteCommand = + ExecuteCommand or function() end, + ---@param strCommand string + ---@param tParams table + function(strCommand, tParams) + for _, callback in pairs(hooks) do + callback(strCommand, tParams) + end + + prevHook(strCommand, tParams) + end +end diff --git a/src/control4-utils/hooks/OnDriverLateInit.lua b/src/control4-utils/hooks/OnDriverLateInit.lua new file mode 100644 index 0000000..89d46c7 --- /dev/null +++ b/src/control4-utils/hooks/OnDriverLateInit.lua @@ -0,0 +1,28 @@ +---@alias C3COnDriverLateInitCallback fun(strDIT: string) +do + if not C3C then + print("Control4Utils: ERROR LOADING src.hooks.OnDriverLateInit, src/base.lua must be required first") + return + end + -- Stores global hook if already defined + local prevHook + + ---@type C3COnDriverLateInitCallback[] + local hooks = {} + + ---@param callback C3COnDriverLateInitCallback + C3C.HookIntoOnDriverLateInit = function(callback) + table.insert(hooks, callback) + end + + prevHook, OnDriverLateInit = + OnDriverLateInit or function() end, + ---@param strDIT string + function(strDIT) + for _, callback in pairs(hooks) do + callback(strDIT) + end + + prevHook(strDIT) + end +end diff --git a/src/control4-utils/hooks/OnPropertyChanged.lua b/src/control4-utils/hooks/OnPropertyChanged.lua new file mode 100644 index 0000000..0a9ae86 --- /dev/null +++ b/src/control4-utils/hooks/OnPropertyChanged.lua @@ -0,0 +1,29 @@ +---@alias C3COnPropertyChangedCallback fun(strProperty: string) +do + if not C3C then + print("Control4Utils: ERROR LOADING src.hooks.OnPropertyChanged, src/base.lua must be required first") + return + end + + -- Stores global hook if already defined + local prevHook + + ---@type C3COnPropertyChangedCallback[] + local hooks = {} + + ---@param callback C3COnPropertyChangedCallback + C3C.HookIntoOnPropertyChanged = function(callback) + table.insert(hooks, callback) + end + + prevHook, OnPropertyChanged = + OnPropertyChanged or function() end, + ---@param strProperty string + function(strProperty) + for _, callback in pairs(hooks) do + callback(strProperty) + end + + prevHook(strProperty) + end +end diff --git a/src/control4-utils/hooks/ReceivedFromProxy.lua b/src/control4-utils/hooks/ReceivedFromProxy.lua new file mode 100644 index 0000000..a454d25 --- /dev/null +++ b/src/control4-utils/hooks/ReceivedFromProxy.lua @@ -0,0 +1,31 @@ +---@alias C3CReceivedFromProxyCallback fun(idBinding: number, strCommand: string, tParams: table) +do + if not C3C then + print("Control4Utils: ERROR LOADING src.hooks.ReceivedFromProxy, src/base.lua must be required first") + return + end + + -- Stores global hook if already defined + local prevHook + + ---@type C3CReceivedFromProxyCallback[] + local hooks = {} + + ---@param callback C3CReceivedFromProxyCallback + C3C.HookIntoReceivedFromProxy = function(callback) + table.insert(hooks, callback) + end + + prevHook, ReceivedFromProxy = + ReceivedFromProxy or function() end, + ---@param idBinding number + ---@param strCommand string + ---@param tParams table + function(idBinding, strCommand, tParams) + for _, callback in pairs(hooks) do + callback(idBinding, strCommand, tParams) + end + + prevHook(idBinding, strCommand, tParams) + end +end diff --git a/src/control4-utils/hooks/all.lua b/src/control4-utils/hooks/all.lua new file mode 100644 index 0000000..cc73881 --- /dev/null +++ b/src/control4-utils/hooks/all.lua @@ -0,0 +1,6 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "ExecuteCommand") +require(dirRequire .. "OnDriverLateInit") +require(dirRequire .. "OnPropertyChanged") +require(dirRequire .. "ReceivedFromProxy") diff --git a/src/control4-utils/knx.lua b/src/control4-utils/knx.lua new file mode 100644 index 0000000..edcf98a --- /dev/null +++ b/src/control4-utils/knx.lua @@ -0,0 +1,3 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "knx.all") diff --git a/src/control4-utils/knx/DPT.lua b/src/control4-utils/knx/DPT.lua new file mode 100644 index 0000000..14acb34 --- /dev/null +++ b/src/control4-utils/knx/DPT.lua @@ -0,0 +1,25 @@ +--- @see https://snap-one.github.io/docs-driverworks-knx/#knx-datapoint-type-overview +--- @alias C3CKnxDPT +---|"DPT_1" +---|"DPT_2" +---|"DPT_3" +---|"DPT_4 +---|"DPT_5" +---|"DPT_6" +---|"DPT_7" +---|"DPT_8" +---|"DPT_9" +---|"DPT_10" +---|"DPT_11" +---|"DPT_12" +---|"DPT_13" +---|"DPT_14" +---|"DPT_15" +---|"DPT_16" +---|"DPT_17" +---|"DPT_18" +---|"DPT_19" +---|"DPT_5_001" +---|"DPT_232" +---|"DPT_242_600" +---|"DPT_251_600" diff --git a/src/control4-utils/knx/GroupAddress.lua b/src/control4-utils/knx/GroupAddress.lua new file mode 100644 index 0000000..08447d2 --- /dev/null +++ b/src/control4-utils/knx/GroupAddress.lua @@ -0,0 +1,235 @@ +---@alias C3CKnxGenericGroupAddressShape { DPT: C3CKnxDPT, GA: string, Name: T, Value: number | nil } + +do + if not C3C then + print("Control4Utils: ERROR LOADING src.knx.GroupAddress, src/base.lua must be required first") + return + end + + ---@param name C3CKnxGroupAddressName + ---@param ga string + ---@param dpt C3CKnxDPT + C3C.KnxGroupAddress = function(name, ga, dpt) + ---@class GroupAddress + ---@field DPT C3CKnxDPT + ---@field GA string + ---@field Name C3CKnxGroupAddressName + ---@field Value number|nil + local class = { + DPT = dpt, + GA = ga, + Name = name, + Value = nil, + } + + local responding = false + + ---@param v number + function class:Send(v) + class.Value = v + C3C.KnxProxy.Send(class.DPT, class.GA, class.Value) + end + + function class:RespondToRead() + responding = true + end + + function class:isResponding() + return responding + end + + return class + end + + ---@type {[C3CKnxGroupAddressName]: GroupAddress} + local namedRegistry = {} + ---@type {[string]: GroupAddress|nil} + local addressedRegistry = {} + + ---@type {[string]: GroupAddress} + local watchedRegistry = {} + + ---@type {[C3CKnxGroupAddressName]: nil|fun(current: GroupAddress, ctx: { newVal: number, prevVal: number?})[]} + local onChangedRegistry = {} + + ---@type {[C3CKnxGroupAddressName]: nil|fun(current: GroupAddress, ctx: { newVal: number, prevVal: number?})[]} + local onReceiveRegistry = {} + + local registered = false + + for _, v in pairs(C3CKnxCreateGroupAddresses()) do + addressedRegistry[v.GA] = v + namedRegistry[v.Name] = v + end + + local registerGAs = function() + C3C.KnxProxy.ClearGroupAddresses() + + C3C.OneShotTimer.Add(3000, function() + registered = true + for _, g in pairs(watchedRegistry) do + C3C.KnxProxy.AddGroupAddress(g) + end + end, "GroupAddressAddGroupItemsToKnx") + end + + C3C.KnxAddresses = { + ---Get GroupAddress by name + ---@param n C3CKnxGroupAddressName + ---@return GroupAddress + Get = function(n) + if namedRegistry[n] == nil then + C3C.Logger.Error( + "error accessing non existing group address name, this should never happen...never", + { name = n } + ) + end + return namedRegistry[n] + end, + + ---Get GroupAddress by address if defined + ---@param ga string + ---@return GroupAddress|nil + GetByGA = function(ga) + return addressedRegistry[ga] + end, + + ---@param n C3CKnxGroupAddressName + ---@param onValueReceive nil|fun(current: GroupAddress, state: { newVal: number, prevVal: number? }) + ---@param onValueChange nil|fun(current: GroupAddress, state: { newVal: number, prevVal: number? }) + Watch = function(n, onValueReceive, onValueChange) + if namedRegistry[n] == nil then + C3C.Logger.Error(W + "error accessing non existing group address name to Listen, this should never happen...never", + { name = n } + ) + end + + local addr = namedRegistry[n] + watchedRegistry[addr.GA] = addr + + if registered then + C3C.OneShotTimer.Add(1000, function() + registerGAs() + end, "knx-register-group-addresses") + end + + if onValueReceive ~= nil then + if onReceiveRegistry[addr.GA] == nil then + onReceiveRegistry[addr.GA] = {} + end + + table.insert(onReceiveRegistry[addr.GA], onValueReceive) + end + + if onValueChange ~= nil then + if onChangedRegistry[addr.GA] == nil then + onChangedRegistry[addr.GA] = {} + end + + table.insert(onChangedRegistry[addr.GA], onValueChange) + end + end, + + ---@param n C3CKnxGroupAddressName + UnWatch = function(n) + local addr = namedRegistry[n] + watchedRegistry[addr.GA] = nil + onReceiveRegistry[addr.GA] = nil + onChangedRegistry[addr.GA] = nil + + if registered then + C3C.OneShotTimer.Add(1000, function() + registerGAs() + end, "knx-register-group-addresses") + end + end, + } + + C3C.HookIntoOnDriverLateInit(function() + if not C3C.KnxProxy or not C3C.OneShotTimer then + C3C.Logger.Error( + "C3C.KnxProxy or C3C.OneShotTimer is not defined", + { fn = "control4-utils.knx.GroupADdress HookIntoOnDriverLateInit" } + ) + return + end + + registerGAs() + end) + + C3C.HookIntoExecuteCommand(function(strCommand, tParams) + if not Trim then + C3C.Logger.Error( + "Trim function is not defined", + { fn = "control4-utils.knx.GroupADdress HookIntoExecuteCommand" } + ) + return + end + + if strCommand == "KNX_READ_REQUEST" then + local ga = Trim(tParams["GROUP_ADDRESS"]) + if ga == "" then + C3C.Logger.Error("received invalid group address from KNX_READ_REQUEST", { + groupAddress = ga, + }) + return + end + + local addr = C3C.KnxAddresses.GetByGA(ga) + if addr ~= nil and addr.Value ~= nil and addr:isResponding() then + addr:Send(addr.Value) + end + end + + if strCommand ~= "DATA_FROM_KNX" then + return + end + + local groupAddress = Trim(tParams["GROUP_ADDRESS"]) + + if groupAddress == "" then + C3C.Logger.Error("received empty group address from DATA_FROM_KNX") + return + end + + local addr = C3C.KnxAddresses.GetByGA(groupAddress) + + if addr == nil then + C3C.Logger.Error("received unknown group address from DATA_FROM_KNX", { + groupAddress = groupAddress, + }) + return + end + + local value = nil + + if addr.DPT == "DPT_3" then + value = tonumber(tParams["DIRECTION"]) + else + value = tonumber(tParams["VALUE"]) + end + + if value == nil then + C3C.Logger.Error("received invalid data from DATA_FROM_KNX, value is nil", { + groupAddress = groupAddress, + }) + return + end + + local prevValue = addr.Value + addr.Value = value + + if onReceiveRegistry[addr.GA] then + for _, callback in pairs(onReceiveRegistry[addr.GA]) do + callback(addr, { newVal = value, prevVal = prevValue }) + end + end + + if onChangedRegistry[addr.GA] and prevValue ~= value then + for _, callback in pairs(onChangedRegistry[addr.GA]) do + callback(addr, { newVal = value, prevVal = prevValue }) + end + end + end) +end diff --git a/src/control4-utils/knx/Proxy.lua b/src/control4-utils/knx/Proxy.lua new file mode 100644 index 0000000..f3bd713 --- /dev/null +++ b/src/control4-utils/knx/Proxy.lua @@ -0,0 +1,68 @@ +do + if not C3C then + print("Control4Utils: ERROR LOADING src.knx.Proxy, src/base.lua must be required first") + return + end + + local binding = nil + + local send = function(strCommand, tParams) + if not binding then + C3C.Logger.Error( + "tried to send data to knx proxy without setting up binding, call C3C.KnxProxy.Setup first", + { + stack = "KnxProxy.send", + } + ) + return false + end + + local ctx = tParams + ctx.knxCommand = strCommand + + C3C.Logger.Debug("sending data to KNX", ctx) + + C4:SendToProxy(binding, strCommand, tParams) + end + + C3C.KnxProxy = { + ---@param bindingId number + Setup = function(bindingId) + binding = bindingId + end, + + ---@param ga GroupAddress + AddGroupAddress = function(ga) + send("ADD_GROUP_ITEM", { + GROUP_ADDRESS = ga.GA, + DEVICE_ID = C4:GetDeviceID(), + PROPERTY = ga.Name, + DATA_POINT_TYPE = ga.DPT, + }) + end, + + ClearGroupAddresses = function() + send("CLEAR_GROUP_ITEMS", { DEVICE_ID = C4:GetDeviceID() }) + end, + + ---@param dpt C3CKnxDPT + ---@param groupAddress string + ---@param value number + Send = function(dpt, groupAddress, value) + local tParams = {} + tParams.DATA_POINT_TYPE = dpt + tParams.GROUP_ADDRESS = groupAddress + tParams.VALUE = value + + send("SEND_TO_KNX", tParams) + end, + + ---@param groupAddress string + Read = function(groupAddress) + local tParams = {} + tParams.GROUP_ADDRESS = groupAddress + + send("REQUEST_STATUS", tParams) + end, + } +end diff --git a/src/control4-utils/knx/all.lua b/src/control4-utils/knx/all.lua new file mode 100644 index 0000000..b151b3f --- /dev/null +++ b/src/control4-utils/knx/all.lua @@ -0,0 +1,7 @@ +local dirRequire = (...):match("(.-)[^%.%/]+$") + +require(dirRequire .. "DPT") +require(dirRequire .. "gen.ADDRESSES") +require(dirRequire .. "gen.createGroupAddresses") +require(dirRequire .. "GroupAddress") +require(dirRequire .. "Proxy") diff --git a/src/control4-utils/knx/gen/ADDRESSES.lua b/src/control4-utils/knx/gen/ADDRESSES.lua new file mode 100644 index 0000000..330859d --- /dev/null +++ b/src/control4-utils/knx/gen/ADDRESSES.lua @@ -0,0 +1,927 @@ +---@alias C3CKnxGroupAddressName +---|C3CKnxGroupAddressNameControl +---|C3CKnxGroupAddressNameDimmer +---|C3CKnxGroupAddressNameHeat +---|C3CKnxGroupAddressNameMedia +---|C3CKnxGroupAddressNameOther +---|C3CKnxGroupAddressNameScene +---|C3CKnxGroupAddressNameSwitch +---|C3CKnxGroupAddressNameVent + +---@alias C3CKnxGroupAddressNameControl +---|"Control LF Hallway Presence" +---|"Control LF Hallway Presence Enable" +---|"Control LF Hallway Presence Enable Workaround" +---|"Control LF Hallway Presence Alarm" +---|"Control LF Hallway Presence Alarm State" +---|"Control LF Study LED Brightness Night" +---|"Control UF Bathroom Presence" +---|"Control UF Bathroom Presence Enable" +---|"Control UF Bathroom Presence Delayed" +---|"Control UF Bathroom Brightness" + +---@alias C3CKnxGroupAddressNameDimmer +--- Hallway Primary +---|"Dimmer LF Hallway" +--- Hallway Primary +---|"Dimmer LF Hallway FB" +--- Hallway Primary +---|"Dimmer LF Hallway Dimmer" +---|"Dimmer LF Hallway Color" +---|"Dimmer LF Hallway Color FB" +---|"Dimmer LF Hallway Color Dimmer" +--- Bathroom Primary +---|"Dimmer LF Bathroom Primary" +--- Bathroom Primary +---|"Dimmer LF Bathroom Primary FB" +--- Bathroom Primary +---|"Dimmer LF Bathroom Primary Dimmer" +---|"Dimmer LF Bathroom Primary Color" +---|"Dimmer LF Bathroom Primary Color FB" +---|"Dimmer LF Bathroom Primary Color Dimmer" +--- Bathroom Ambient +--- HAS UNKNOWN DPT +---|"Dimmer LF Bathroom Ambient" +--- Bathroom Ambient +--- HAS UNKNOWN DPT +---|"Dimmer LF Bathroom Ambient FB" +--- Bathroom Ambient +---|"Dimmer LF Bathroom Ambient Dimmer" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bathroom Ambient HSV" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bathroom Ambient HSV FB" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bathroom Ambient Hue Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bathroom Ambient Saturation Dimming" +--- Bathroom Vanity +---|"Dimmer LF Bathroom Vanity" +--- Bathroom Vanity +---|"Dimmer LF Bathroom Vanity FB" +--- Bathroom Vanity +---|"Dimmer LF Bathroom Vanity Dimmer" +---|"Dimmer LF Bathroom Vanity Color" +---|"Dimmer LF Bathroom Vanity Color FB" +---|"Dimmer LF Bathroom Vanity Color Dimmer" +--- Laundry Primary +---|"Dimmer LF Laundry Primary" +--- Laundry Primary +---|"Dimmer LF Laundry Primary FB" +--- Laundry Primary +---|"Dimmer LF Laundry Primary Dimmer" +---|"Dimmer LF Laundry Color" +---|"Dimmer LF Laundry Color FB" +---|"Dimmer LF Laundry Color Dimmer" +--- Bedroom 1 Primary +---|"Dimmer LF Bedroom 1 Primary" +--- Bedroom 1 Primary +---|"Dimmer LF Bedroom 1 Primary FB" +--- Bedroom 1 Primary +---|"Dimmer LF Bedroom 1 Primary Dimmer" +---|"Dimmer LF Bedroom 1 Primary Color" +---|"Dimmer LF Bedroom 1 Primary Color FB" +---|"Dimmer LF Bedroom 1 Primary Color Dimmer" +--- Bedroom 1 Ambient +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 1 Ambient" +--- Bedroom 1 Ambient +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 1 Ambient FB" +--- Bedroom 1 Ambient +---|"Dimmer LF Bedroom 1 Ambient Dimmer" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 1 Ambient HSV" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 1 Ambient HSV FB" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 1 Ambient Hue Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 1 Ambient Saturation Dimming" +--- Study Primary +---|"Dimmer LF Study Primary" +--- Study Primary +---|"Dimmer LF Study Primary FB" +--- Study Primary +---|"Dimmer LF Study Primary Dimmer" +---|"Dimmer LF Study Color" +---|"Dimmer LF Study Color FB" +---|"Dimmer LF Study Color Dimmer" +--- Study Ambient +--- HAS UNKNOWN DPT +---|"Dimmer LF Study Ambient" +--- Study Ambient +--- HAS UNKNOWN DPT +---|"Dimmer LF Study Ambient FB" +--- Study Ambient +---|"Dimmer LF Study Ambient Dimmer" +--- HAS UNKNOWN DPT +---|"Dimmer LF Study Ambient HSV" +--- HAS UNKNOWN DPT +---|"Dimmer LF Study Ambient HSV FB" +--- HAS UNKNOWN DPT +---|"Dimmer LF Study Ambient Hue Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer LF Study Ambient Saturation Dimming" +--- Bedroom 2 Primary +---|"Dimmer LF Bedroom 2 Primary" +--- Bedroom 2 Primary +---|"Dimmer LF Bedroom 2 Primary FB" +--- Bedroom 2 Primary +---|"Dimmer LF Bedroom 2 Primary Dimmer" +---|"Dimmer LF Bedroom 2 Primary Color" +---|"Dimmer LF Bedroom 2 Primary Color FB" +---|"Dimmer LF Bedroom 2 Primary Color Dimmer" +--- Bedroom 2 Ambient +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 2 Ambient" +--- Bedroom 2 Ambient +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 2 Ambient FB" +--- Bedroom 2 Ambient +---|"Dimmer LF Bedroom 2 Ambient Dimmer" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 2 Ambient HSV" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 2 Ambient HSV FB" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 2 Ambient Hue Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer LF Bedroom 2 Ambient Saturation Dimming" +--- Utility room Primary +---|"Dimmer LF Utility" +--- Utility room Primary +---|"Dimmer LF Utility FB" +--- Utility room Primary +---|"Dimmer LF Utility Dimmer" +---|"Dimmer LF Utility Color" +---|"Dimmer LF Utility Color FB" +---|"Dimmer LF Utility Color Dimmer" +--- Hallway Entry +---|"Dimmer UF Hallway" +--- Hallway Entry +---|"Dimmer UF Hallway FB" +--- Hallway Entry +---|"Dimmer UF Hallway Dimming" +--- Hallway Entry +---|"Dimmer UF Hallway Color" +--- Hallway Entry +---|"Dimmer UF Hallway Color FB" +--- Hallway Entry +---|"Dimmer UF Hallway Color Dimming" +--- Bathroom Primary +---|"Dimmer UF Bathroom Primary" +--- Bathroom Primary +---|"Dimmer UF Bathroom Primary FB" +--- Bathroom Primary +---|"Dimmer UF Bathroom Primary Dimming" +---|"Dimmer UF Bathroom Primary Color" +---|"Dimmer UF Bathroom Primary Color FB" +---|"Dimmer UF Bathroom Primary Color Dimming" +--- Bathroom Ambient +---|"Dimmer UF Bathroom Ambient" +--- Bathroom Ambient +---|"Dimmer UF Bathroom Ambient FB" +--- Bathroom Ambient +---|"Dimmer UF Bathroom Ambient Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer UF Bathroom Ambient HSV" +--- HAS UNKNOWN DPT +---|"Dimmer UF Bathroom Ambient HSV FB" +---|"Dimmer UF Bathroom Ambient Hue Dimming" +---|"Dimmer UF Bathroom Ambient Saturation Dimming" +--- Bathroom Vanity +---|"Dimmer UF Bathroom Vanity" +--- Bathroom Vanity +---|"Dimmer UF Bathroom Vanity FB" +--- Bathroom Vanity +---|"Dimmer UF Bathroom Vanity Dimming" +---|"Dimmer UF Bathroom Vanity Color" +---|"Dimmer UF Bathroom Vanity Color FB" +---|"Dimmer UF Bathroom Vanity Color Dimming" +--- Bedroom Primary +---|"Dimmer UF Bedroom Primary" +--- Bedroom Primary +---|"Dimmer UF Bedroom Primary FB" +--- Bedroom Primary +---|"Dimmer UF Bedroom Primary Dimming" +---|"Dimmer UF Bedroom Primary Color" +---|"Dimmer UF Bedroom Primary Color FB" +---|"Dimmer UF Bedroom Primary Color Dimming" +--- Bedroom Ambient +---|"Dimmer UF Bedroom Ambient" +--- Bedroom Ambient +--- HAS UNKNOWN DPT +---|"Dimmer UF Bedroom Ambient FB" +--- Bedroom Ambient +---|"Dimmer UF Bedroom Ambient Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer UF Bedroom Ambient HSV" +--- HAS UNKNOWN DPT +---|"Dimmer UF Bedroom Ambient HSV FB" +--- HAS UNKNOWN DPT +---|"Dimmer UF Bedroom Ambient Hue Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer UF Bedroom Ambient Saturation Dimming" +--- Bedroom Closet +---|"Dimmer UF Closet" +--- Bedroom Closet +---|"Dimmer UF Closet FB" +--- Bedroom Closet +---|"Dimmer UF Closet Dimming" +---|"Dimmer UF Closet Color" +---|"Dimmer UF Closet Color FB" +---|"Dimmer UF Closet Color Dimming" +--- Living room Primary +---|"Dimmer UF Living room Primary" +--- Living room Primary +---|"Dimmer UF Living room Primary FB" +--- Living room Primary +---|"Dimmer UF Living room Primary Dimming" +--- Living room Primary +---|"Dimmer UF Living room Primary Color" +--- Living room Primary +---|"Dimmer UF Living room Primary Color FB" +--- Living room Primary +---|"Dimmer UF Living room Primary Color Dimming" +--- Living room Ambient +---|"Dimmer UF Living room Ambient" +--- Living room Ambient +---|"Dimmer UF Living room Ambient FB" +--- Living room Ambient +---|"Dimmer UF Living room Ambient Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer UF Living room Ambient HSV" +--- HAS UNKNOWN DPT +---|"Dimmer UF Living room Ambient HSV FB" +--- Living room Ambient +---|"Dimmer UF Living room Ambient Hue Dimming" +--- Living room Ambient +---|"Dimmer UF Living room Ambient Saturation Dimming" +--- Office Primary +---|"Dimmer UF Office Primary" +--- Office Primary +---|"Dimmer UF Office Primary FB" +--- Office Primary +---|"Dimmer UF Office Primary Dimming" +---|"Dimmer UF Office Color" +---|"Dimmer UF Office Color FB" +---|"Dimmer UF Office Color Dimming" +--- Dining room Primary +---|"Dimmer UF Dining room" +--- Dining room Primary +---|"Dimmer UF Dining room FB" +--- Dining room Primary +---|"Dimmer UF Dining room Dimming" +---|"Dimmer UF Dining Room Color" +---|"Dimmer UF Dining Room Color FB" +---|"Dimmer UF Dining Room Color Dimming" +--- Kitchen Primary +---|"Dimmer UF Kitchen Primary" +--- Kitchen Primary +---|"Dimmer UF Kitchen Primary FB" +--- Kitchen Primary +---|"Dimmer UF Kitchen Primary Dimming" +---|"Dimmer UF Kitchen Primary Color" +---|"Dimmer UF Kitchen Primary Color FB" +---|"Dimmer UF Kitchen Primary Color Dimming" +--- Kitchen Ambient +--- HAS UNKNOWN DPT +---|"Dimmer UF Kitchen Ambient" +--- Kitchen Ambient +--- HAS UNKNOWN DPT +---|"Dimmer UF Kitchen Ambient FB" +--- Kitchen Ambient +--- HAS UNKNOWN DPT +---|"Dimmer UF Kitchen Ambient Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer UF Kitchen Ambient HSV" +--- HAS UNKNOWN DPT +---|"Dimmer UF Kitchen Ambient HSV FB" +--- HAS UNKNOWN DPT +---|"Dimmer UF Kitchen Ambient Hue Dimming" +--- HAS UNKNOWN DPT +---|"Dimmer UF Kitchen Ambient Saturation Dimming" +--- Kitchen Counter +---|"Dimmer UF Kitchen Counter" +--- Kitchen Counter +---|"Dimmer UF Kitchen Counter FB" +--- Kitchen Counter +---|"Dimmer UF Kitchen Counter Dimming" +---|"Dimmer UF Kitchen Counter Color" +---|"Dimmer UF Kitchen Counter Color FB" +---|"Dimmer UF Kitchen Counter Color Dimming" +--- Kitchen Counter +---|"Dimmer UF Pantry" +--- Kitchen Counter +---|"Dimmer UF Pantry FB" +--- Kitchen Counter +---|"Dimmer UF Pantry Dimming" +---|"Dimmer UF Pantry Color" +---|"Dimmer UF Pantry Color FB" +---|"Dimmer UF Pantry Color Dimming" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase Tree Dimming" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase Tree Value" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase Tree Value Feedback" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase handrail Swtich" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase handrail Value" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase handrail Value Feedback" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase handrail bottom Dimming" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase handrail bottom Value" +--- Staircase Primary +--- HAS UNKNOWN DPT +---|"Dimmer Staircase handrail bottom Value Feedback" +--- 20_Garden Terrace +--- HAS UNKNOWN DPT +---|"Dimmer EXT Terrace Dimming" +--- 20_Garden Terrace +--- HAS UNKNOWN DPT +---|"Dimmer EXT Terrace Value" +--- 20_Garden Terrace +--- HAS UNKNOWN DPT +---|"Dimmer EXT Terrace Value Feedback" +--- 20_Exterior Entrance +--- HAS UNKNOWN DPT +---|"Dimmer EXT Entrance Value Feedback" +--- 20_Exterior Entrance +--- HAS UNKNOWN DPT +---|"Dimmer EXT Entrance Dimming" +--- 20_Exterior Entrance +--- HAS UNKNOWN DPT +---|"Dimmer EXT Entrance Value" +--- Parking place Primary +--- HAS UNKNOWN DPT +---|"Dimmer Parking place Dimming" +--- Parking place Primary +--- HAS UNKNOWN DPT +---|"Dimmer Parking place Value" +--- Parking place Primary +--- HAS UNKNOWN DPT +---|"Dimmer Parking place Value Feedback" + +---@alias C3CKnxGroupAddressNameHeat +---|"Heat LF Hallway Temperature FB" +---|"Heat LF Bathroom Temperature FB" +---|"Heat LF Bathroom Ladder boost" +---|"Heat LF Bathroom Ladder boost FB" +--- HAS UNKNOWN DPT +---|"Heat LF Laundry Temperature FB" +---|"Heat LF Bedroom 1 Temperature FB" +---|"Heat LF Study Temperature FB" +---|"Heat LF Bedroom 2 Temperature FB" +---|"Heat LF Technical Room Temperature FB" +---|"Heat UF Hallway Temperature FB" +---|"Heat UF Bathroom Temperature FB" +---|"Heat UF Bathroom Ladder boost" +---|"Heat UF Bathroom Ladder boost FB" +---|"Heat UF Bedroom Temperature FB" +---|"Heat UF Living Room Temperature FB" +---|"Heat UF Kitchen Temperature FB" +---|"Heat Circulation Pump" +---|"Heat Circulation Pump FB" +---|"Heat Hot Water Circulation State" +---|"Heat Hot Water Circulation Emergency State" +---|"Heat Hot Water Circulation Control Value" +--- Heating = 1 +--- Cooling = 0 +---|"Heat Heating-Cooling" +--- Heating = 1 +--- Cooling = 0 +---|"Heat Heating-Cooling State" +--- HAS UNKNOWN DPT +---|"Heat Temperature Setpoint" +--- HAS UNKNOWN DPT +---|"Heat Temperature Setpoint State" +---|"Heat Heating Off lock" +---|"Heat UF+LF Heating Circulation Pump" +---|"Heat UF+LF Heating Circulation Pump State" + +---@alias C3CKnxGroupAddressNameMedia +---|"Media UF Bathroom Playing" +---|"Media UF Bathroom Off" +---|"Media UF Bathroom Volume Down" +---|"Media UF Bathroom Volume Up" +---|"Media UF Bathroom Previous" +---|"Media UF Bathroom Next" +---|"Media UF Bathroom Preset Cycle Forward" + +---@alias C3CKnxGroupAddressNameOther +---|"Entry" +---|"Exit" +---|"Present" +---|"UF Bathroom Exit" +---|"LF Bathroom Exit" +--- Day = 0 +--- Night = 1 +---|"Day/Night" +---|"Date" +---|"Time" +---|"Led Intensity" +---|"Retention Tank Water Level Low" + +---@alias C3CKnxGroupAddressNameScene +---|"Scene Lower Floor" +---|"Scene Lower Floor Hallway" +---|"Scene Lower Floor Bathroom" +---|"Scene Lower Floor Laundry" +---|"Scene Lower Floor Bedroom 1" +---|"Scene Lower Floor Study" +---|"Scene Lower Floor Bedroom 2" +---|"Scene Lower Floor Technical Room" +---|"Scene Upper Floor" +---|"Scene Upper Floor Hallway" +---|"Scene Upper Floor Bathroom" +---|"Scene Upper Floor Bedroom" +---|"Scene Upper Floor Closet" +---|"Scene Upper Floor Living Room" +---|"Scene Upper Floor Office" +---|"Scene Upper Floor Dining Room" +---|"Scene Upper Floor Kitchen" +---|"Scene Upper Floor Pantry" +---|"Scene Staircase" +---|"Scene Swimming Pool" +---|"Scene Ventilation Valves" +---|"Scene Ventilation Work" + +---@alias C3CKnxGroupAddressNameSwitch +--- Hallway Primary +---|"Switch LF Hallway" +--- Hallway Under staircase +---|"Switch LF Hallway CTRL" +--- Hallway Primary +---|"Switch LF Hallway Light 1" +--- Hallway Primary +---|"Switch LF Hallway Light 2" +--- Hallway Under staircase +---|"Switch LF Hallway Light Below staircase" +--- Hallway Primary +---|"Switch LF Hallway FB" +--- Hallway Under staircase +---|"Switch LF Hallway CTRL FB" +--- Hallway Primary +---|"Switch LF Hallway Light 1 FB" +--- Hallway Primary +---|"Switch LF Hallway Light 2 FB" +--- Hallway Under staircase +---|"Switch LF Hallway Light Below staircase FB" +--- Bathroom Primary +---|"Switch LF Bathroom Primary" +--- Bathroom Vanity +---|"Switch LF Bathroom Primary CTRL" +--- Bathroom Primary +---|"Switch LF Bathroom Primary Light 1" +--- Bathroom Primary +---|"Switch LF Bathroom Primary Light 2" +--- Bathroom Vanity +---|"Switch LF Bathroom Primary Light Vanity" +---|"Switch LF Bathroom Primary Ceiling" +--- Bathroom Primary +---|"Switch LF Bathroom Primary FB" +--- Bathroom Vanity +---|"Switch LF Bathroom Primary CTRL FB" +--- Bathroom Primary +---|"Switch LF Bathroom Primary Light 1 FB" +--- Bathroom Primary +---|"Switch LF Bathroom Primary Light 2 FB" +--- Bathroom Vanity +---|"Switch LF Bathroom Primary Light Vanity FB" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient CTRL" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient Light 1" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient Light 2" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient Toilet" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient FB" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient CTRL FB" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient Light 1 FB" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient Light 2 FB" +--- Bathroom Ambient +---|"Switch LF Bathroom Ambient Toilet FB" +--- Laundry Primary +---|"Switch LF Laundry Primary" +--- Laundry Primary +---|"Switch LF Laundry Primary CTRL" +--- Laundry Primary +---|"Switch LF Laundry Primary Light 1" +--- Laundry Primary +---|"Switch LF Laundry Primary Light 2" +--- Laundry Primary +---|"Switch LF Laundry Primary FB" +--- Laundry Primary +---|"Switch LF Laundry Primary CTRL FB" +--- Laundry Primary +---|"Switch LF Laundry Primary Light 1 FB" +--- Laundry Primary +---|"Switch LF Laundry Primary Light 2 FB" +--- Bedroom 1 Primary +---|"Switch LF Bedroom 1 Primary" +--- Bedroom 1 Reading light +---|"Switch LF Bedroom 1 Primary CTRL" +--- Bedroom 1 Primary +---|"Switch LF Bedroom 1 Primary Light 1" +--- Bedroom 1 Primary +---|"Switch LF Bedroom 1 Primary Light 2" +--- Bedroom 1 Reading light +---|"Switch LF Bedroom 1 Primary Light Reading" +--- Bedroom 1 Primary +---|"Switch LF Bedroom 1 Primary FB" +---|"Switch LF Bedroom 1 Primary CTRL FB" +--- Bedroom 1 Primary +---|"Switch LF Bedroom 1 Primary Light 1 FB" +--- Bedroom 1 Primary +---|"Switch LF Bedroom 1 Primary Light 2 FB" +--- Bedroom 1 Reading light +---|"Switch LF Bedroom 1 Primary Light Reading FB" +--- Bedroom 1 Ambient +---|"Switch LF Bedroom 1 Ambient" +--- Bedroom 1 Ambient +---|"Switch LF Bedroom 1 Ambient CTRL" +--- Bedroom 1 Ambient +---|"Switch LF Bedroom 1 Ambient Light 1" +--- Bedroom 1 Ambient +---|"Switch LF Bedroom 1 Ambient Light 2" +--- Bedroom 1 Ambient +---|"Switch LF Bedroom 1 Ambient FB" +--- Bedroom 1 Ambient +---|"Switch LF Bedroom 1 Ambient CTRL FB" +--- Bedroom 1 Ambient +---|"Switch LF Bedroom 1 Ambient Light 1 FB" +--- Bedroom 1 Ambient +---|"Switch LF Bedroom 1 Ambient Light 2 FB" +--- Study Primary +---|"Switch LF Study Primary" +--- Study Primary +---|"Switch LF Study Primary CTRL" +--- Study Primary +---|"Switch LF Study Primary Light 1" +--- Study Primary +---|"Switch LF Study Primary Light 2" +--- Study Primary +---|"Switch LF Study Primary FB" +--- Study Primary +---|"Switch LF Study Primary CTRL FB" +--- Study Primary +---|"Switch LF Study Primary Light 1 FB" +--- Study Primary +---|"Switch LF Study Primary Light 2 FB" +--- Study Ambient +---|"Switch LF Study Ambient" +--- Study Ambient +---|"Switch LF Study Ambient CTRL" +--- Study Ambient +---|"Switch LF Study Ambient Light 1" +--- Study Ambient +---|"Switch LF Study Ambient Light 2" +--- Study Ambient +---|"Switch LF Study Ambient FB" +--- Study Ambient +---|"Switch LF Study Ambient CTRL FB" +--- Study Ambient +---|"Switch LF Study Ambient Light 1 FB" +--- Study Ambient +---|"Switch LF Study Ambient Light 2 FB" +--- Bedroom 2 Primary +---|"Switch LF Bedroom 2 Primary" +--- Bedroom 2 Reading light +---|"Switch LF Bedroom 2 Primary CTRL" +--- Bedroom 2 Primary +---|"Switch LF Bedroom 2 Primary Light 1" +--- Bedroom 2 Primary +---|"Switch LF Bedroom 2 Primary Light 2" +--- Bedroom 2 Reading light +---|"Switch LF Bedroom 2 Primary Light Reading" +--- Bedroom 2 Primary +---|"Switch LF Bedroom 2 Primary FB" +--- Bedroom 2 Reading light +---|"Switch LF Bedroom 2 Primary CTRL FB" +--- Bedroom 2 Primary +---|"Switch LF Bedroom 2 Primary Light 1 FB" +--- Bedroom 2 Primary +---|"Switch LF Bedroom 2 Primary Light 2 FB" +--- Bedroom 2 Reading light +---|"Switch LF Bedroom 2 Primary Light Reading FB" +--- Bedroom 2 Ambient +---|"Switch LF Bedroom 2 Ambient" +--- Bedroom 2 Ambient +---|"Switch LF Bedroom 2 Ambient CTRL" +--- Bedroom 2 Ambient +---|"Switch LF Bedroom 2 Ambient Light 1" +--- Bedroom 2 Ambient +---|"Switch LF Bedroom 2 Ambient Light 2" +--- Bedroom 2 Ambient +---|"Switch LF Bedroom 2 Ambient FB" +--- Bedroom 2 Ambient +---|"Switch LF Bedroom 2 Ambient CTRL FB" +--- Bedroom 2 Ambient +---|"Switch LF Bedroom 2 Ambient Light 1 FB" +--- Bedroom 2 Ambient +---|"Switch LF Bedroom 2 Ambient Light 2 FB" +---|"Switch LF Utility" +---|"Switch LF Utility CTRL" +---|"Switch LF Utility Light 1" +---|"Switch LF Utility Counter" +---|"Switch LF Utility FB" +---|"Switch LF Utility CTRL FB" +---|"Switch LF Utility Light 1 FB" +---|"Switch LF Utility Counter FB" +---|"Switch UF Hallway Entry" +---|"Switch UF Hallway Isle" +---|"Switch UF Hallway CTRL" +---|"Switch UF Hallway Entry 1" +---|"Switch UF Hallway Entry 2" +---|"Switch UF Hallway Isle 1" +---|"Switch UF Hallway Isle 2" +---|"Switch UF Hallway Entry FB" +---|"Switch UF Hallway Isle FB" +---|"Switch UF Hallway CTRL FB" +---|"Switch UF Hallway Entry 1 FB" +---|"Switch UF Hallway Entry 2 FB" +---|"Switch UF Hallway Isle 1 FB" +---|"Switch UF Hallway Isle 2 FB" +---|"Switch UF Bathroom Primary" +---|"Switch UF Bathroom Primary CTRL" +---|"Switch UF Bathroom Primary Light 1" +---|"Switch UF Bathroom Primary Light 2" +---|"Switch UF Bathroom Primary Light Vanity" +---|"Switch UF Bathroom Primary Ceiling" +---|"Switch UF Bathroom Primary FB" +---|"Switch UF Bathroom Primary CTRL FB" +---|"Switch UF Bathroom Primary Light 1 FB" +---|"Switch UF Bathroom Primary Light 2 FB" +---|"Switch UF Bathroom Primary Light Vanity FB" +---|"Switch UF Bathroom Vanity CTRL" +---|"Switch UF Bathroom Vanity CTRL FB" +---|"Switch UF Bathroom Ambient" +---|"Switch UF Bathroom Ambient CTRL" +---|"Switch UF Bathroom Ambient Light 1" +---|"Switch UF Bathroom Ambient Light 2" +---|"Switch UF Bathroom Ambient Toilet" +---|"Switch UF Bathroom Ambient FB" +---|"Switch UF Bathroom Ambient CTRL FB" +---|"Switch UF Bathroom Ambient Light 1 FB" +---|"Switch UF Bathroom Ambient Light 2 FB" +---|"Switch UF Bathroom Ambient Toilet FB" +---|"Switch UF Bedroom Primary" +---|"Switch UF Bedroom Primary CTRL" +---|"Switch UF Bedroom Primary Light 1" +---|"Switch UF Bedroom Primary Light 2" +---|"Switch UF Bedroom Primary Reading 1" +---|"Switch UF Bedroom Primary Reading 2" +---|"Switch UF Bedroom Primary FB" +---|"Switch UF Bedroom Primary CTRL FB" +---|"Switch UF Bedroom Primary Light 1 FB" +---|"Switch UF Bedroom Primary Light 2 FB" +---|"Switch UF Bedroom Primary Reading 1 FB" +---|"Switch UF Bedroom Primary Reading 2 FB" +---|"Switch UF Bedroom Ambient" +---|"Switch UF Bedroom Ambient CTRL" +---|"Switch UF Bedroom Ambient Light 1" +---|"Switch UF Bedroom Ambient Light 2" +---|"Switch UF Bedroom Ambient FB" +---|"Switch UF Bedroom Ambient CTRL FB" +---|"Switch UF Bedroom Ambient Light 1 FB" +---|"Switch UF Bedroom Ambient Light 2 FB" +---|"Switch UF Closet" +---|"Switch UF Closet CTRL" +---|"Switch UF Closet Light 1" +---|"Switch UF Closet Light 2" +---|"Switch UF Closet FB" +---|"Switch UF Closet CTRL FB" +---|"Switch UF Closet Light 1 FB" +---|"Switch UF Closet Light 2 FB" +---|"Switch UF Living room Primary" +---|"Switch UF Living room Primary CTRL" +---|"Switch UF Living room Primary Light 1" +---|"Switch UF Living room Primary Light 2" +---|"Switch UF Living room Primary Light 3" +---|"Switch UF Living room Primary FB" +---|"Switch UF Living room Primary CTRL FB" +---|"Switch UF Living room Primary Light 1 FB" +---|"Switch UF Living room Primary Light 2 FB" +---|"Switch UF Living room Primary Light 3 FB" +---|"Switch UF Living room Ambient" +---|"Switch UF Living room Ambient CTRL" +---|"Switch UF Living room Ambient Light 1" +---|"Switch UF Living room Ambient Light 2" +---|"Switch UF Living room Ambient FB" +---|"Switch UF Living room Ambient CTRL FB" +---|"Switch UF Living room Ambient Light 1 FB" +---|"Switch UF Living room Ambient Light 2 FB" +---|"Switch UF Office Primary" +---|"Switch UF Office Primary CTRL" +---|"Switch UF Office Primary Light 1" +---|"Switch UF Office Primary Light 2" +---|"Switch UF Office Primary FB" +---|"Switch UF Office Primary CTRL FB" +---|"Switch UF Office Primary Light 1 FB" +---|"Switch UF Office Primary Light 2 FB" +---|"Switch UF Dining room" +---|"Switch UF Dining room CTRL" +---|"Switch UF Dining room Light 1" +---|"Switch UF Dining room Light 2" +---|"Switch UF Dining room FB" +---|"Switch UF Dining room CTRL FB" +---|"Switch UF Dining room Light 1 FB" +---|"Switch UF Dining room Light 2 FB" +---|"Switch UF Kitchen Primary" +---|"Switch UF Kitchen Primary CTRL" +---|"Switch UF Kitchen Primary Light 1" +---|"Switch UF Kitchen Primary Light 2" +---|"Switch UF Kitchen Primary Light 3" +---|"Switch UF Kitchen Primary FB" +---|"Switch UF Kitchen Primary CTRL FB" +---|"Switch UF Kitchen Primary Light 1 FB" +---|"Switch UF Kitchen Primary Light 2 FB" +---|"Switch UF Kitchen Primary Light 3 FB" +---|"Switch UF Kitchen Ambient" +---|"Switch UF Kitchen Ambient CTRL" +---|"Switch UF Kitchen Ambient Light 1" +---|"Switch UF Kitchen Ambient Light 2" +---|"Switch UF Kitchen Ambient Light 3" +---|"Switch UF Kitchen Ambient FB" +---|"Switch UF Kitchen Ambient CTRL FB" +---|"Switch UF Kitchen Ambient Light 1 FB" +---|"Switch UF Kitchen Ambient Light 2 FB" +---|"Switch UF Kitchen Ambient Light 3 FB" +---|"Switch UF Kitchen Counter" +---|"Switch UF Kitchen Counter CTRL" +---|"Switch UF Kitchen Counter Light 1" +---|"Switch UF Kitchen Counter Light 2" +---|"Switch UF Kitchen Counter Light 3" +---|"Switch UF Kitchen Counter FB" +---|"Switch UF Kitchen Counter CTRL FB" +---|"Switch UF Kitchen Counter Light 1 FB" +---|"Switch UF Kitchen Counter Light 2 FB" +---|"Switch UF Kitchen Counter Light 3 FB" +---|"Switch UF Pantry" +---|"Switch UF Pantry CTRL" +---|"Switch UF Pantry Light 1" +---|"Switch UF Pantry Light 2" +---|"Switch UF Pantry FB" +---|"Switch UF Pantry CTRL FB" +---|"Switch UF Pantry Light 1 FB" +---|"Switch UF Pantry Light 2 FB" +---|"Switch Staircase" +---|"Switch Staircase CTRL" +--- Staircase Primary +---|"Switch Staircase Tree" +--- Staircase Primary +---|"Switch Staircase Handrail" +---|"Switch Staircase FB" +---|"Switch Staircase CTRL FB" +--- Staircase Primary +---|"Switch Staircase Tree FB" +--- Staircase Primary +---|"Switch Staircase Handrail FB" +--- 20_Garden Terrace +---|"Switch EXT Terrace" +--- 20_Garden Terrace +---|"Switch EXT Terrace FB" +--- 20_Exterior Entrance +---|"Switch EXT Entrance" +--- 20_Exterior Entrance +---|"Switch EXT Entrance FB" +--- Parking place Primary +---|"Switch Parking place" +--- Parking place Primary +---|"Switch Parking place FB" +---|"Switch Parking place AC Plug Right of Door" +---|"Switch Parking place AC Plug Right of Door State" +---|"Switch Underwater Light" +---|"Switch Underwater Light FB" +---|"Switch Filtration" +---|"Switch Filtration FB" + +---@alias C3CKnxGroupAddressNameVent +--- HAS UNKNOWN DPT +---|"Vent LF PLACEHOLDER Hallway" +---|"Vent LF Hallway Valve IN" +---|"Vent LF Hallway Valve IN FB" +---|"Vent LF Bathroom CO2 FB" +---|"Vent LF Bathroom Humidity FB" +---|"Vent LF Bathroom boost" +---|"Vent LF Bathroom boost FB" +---|"Vent LF Bathroom Valve IN" +---|"Vent LF Bathroom Valve IN FB" +---|"Vent LF Bathroom Valve OUT" +---|"Vent LF Bathroom Valve OUT FB" +--- HAS UNKNOWN DPT +---|"Vent LF PLACEHOLDER Laundry" +---|"Vent LF Bedroom 1 CO2 FB" +---|"Vent LF Bedroom 1 Humidity FB" +---|"Vent LF Bedroom 1 Valve IN" +---|"Vent LF Bedroom 1 Valve IN FB" +---|"Vent LF Study CO2 FB" +---|"Vent LF Study Humidity FB" +---|"Vent LF Study Valve IN" +---|"Vent LF Study Valve IN FB" +---|"Vent LF Study Valve IN Lock" +---|"Vent LF Bedroom 2 CO2 FB" +---|"Vent LF Bedroom 2 Humidity FB" +---|"Vent LF Bedroom 2 Valve IN" +---|"Vent LF Bedroom 2 Valve IN FB" +--- HAS UNKNOWN DPT +---|"Vent LF Technical Room CO2 FB" +---|"Vent LF Technical Room Humidity FB" +---|"Vent LF Box Valve OUT" +---|"Vent LF Box Valve OUT FB" +---|"Vent LF Box Valve OUT Lock" +--- HAS UNKNOWN DPT +---|"Vent UF PLACEHOLDER Hallway" +---|"Vent UF Bathroom CO2 FB" +---|"Vent UF Bathroom Humidity FB" +---|"Vent UF Bathroom boost" +---|"Vent UF Bathroom boost FB" +---|"Vent UF Bathroom Valve IN" +---|"Vent UF Bathroom Valve IN FB" +---|"Vent UF Bathroom Valve IN Lock" +--- HAS UNKNOWN DPT +---|"Vent UF PLACEHOLDER Bedroom" +---|"Vent UF Bedroom Valve IN" +---|"Vent UF Bedroom Valve IN FB" +---|"Vent UF Bedroom Valve IN Lock" +---|"Vent UF Bedroom CO2 FB" +---|"Vent UF Bedroom Humidity FB" +---|"Vent UF Living Room CO2 FB" +---|"Vent UF Living Room Humidity FB" +---|"Vent UF Living Room Valve IN" +---|"Vent UF Living Room Valve IN FB" +--- HAS UNKNOWN DPT +---|"Vent UF PLACEHOLDER Office" +---|"Vent UF Office Valve IN" +---|"Vent UF Office Valve IN FB" +---|"Vent UF Bathroom Valve OUT" +---|"Vent UF Bathroom Valve OUT FB" +---|"Vent UF Bathroom Valve OUT Lock" +--- HAS UNKNOWN DPT +---|"Vent UF PLACEHOLDER Dining Room" +--- HAS UNKNOWN DPT +---|"Vent UF PLACEHOLDER Kitchen" +---|"Vent UF Kitchen Valve OUT" +---|"Vent UF Kitchen Valve OUT FB" +---|"Vent UF Kitchen CO2 FB" +---|"Vent UF Kitchen Humidity FB" +---|"Vent UF Kitchen TVOC FB" +---|"Vent UF Kitchen boost" +---|"Vent UF Kitchen boost FB" +---|"Vent UF Closet+Hallway Valve OUT" +---|"Vent UF Closet+Hallway Valve OUT FB" +---|"Vent UF Closet+Hallway Valve OUT Lock" +---|"Vent Airflow" +---|"Vent Standby FB" +---|"Vent Temperature: Supply" +---|"Vent Temperature: Outdoor" +---|"Vent Temperature: Extract" +---|"Vent Temperature: Exhaust" +---|"Vent Humidity: Supply" +---|"Vent Humidity: Outdoor" +---|"Vent Humidity: Extract" +---|"Vent Humidity: Exhaust" +---|"Vent Filter: Remaining (h)" +---|"Vent Filter: Replace" +---|"Vent Heating-Cooling" +---|"Vent Heating-Cooling State" +---|"Vent Boost" +---|"Vent Boost FB" +---|"Vent Auto mode" +---|"Vent Auto mode FB" +---|"Vent Preset" +---|"Vent Preset FB" +---|"Vent Preset Away" +---|"Vent Preset Away FB" +---|"Vent Away function" +---|"Vent Away function FB" +---|"Vent Temperature profile mode" +---|"Vent Temperature profile mode FB" +---|"Vent Temperature profile" +---|"Vent Temperature profile FB" +---|"Vent Boost Time" +---|"Vent Boost Time FB" +---|"Vent External setpoint" +---|"Vent External setpoint FB" +---|"Vent Error" +---|"Vent Status" diff --git a/src/control4-utils/knx/gen/createGroupAddresses.lua b/src/control4-utils/knx/gen/createGroupAddresses.lua new file mode 100644 index 0000000..8acfe14 --- /dev/null +++ b/src/control4-utils/knx/gen/createGroupAddresses.lua @@ -0,0 +1,628 @@ + +---@return table +function C3CKnxCreateGroupAddresses() + return { + C3C.KnxGroupAddress("Entry", "0/0/1", "DPT_1"), + C3C.KnxGroupAddress("Exit", "0/0/2", "DPT_1"), + C3C.KnxGroupAddress("Present", "0/0/3", "DPT_1"), + C3C.KnxGroupAddress("UF Bathroom Exit", "0/0/50", "DPT_1"), + C3C.KnxGroupAddress("LF Bathroom Exit", "0/0/100", "DPT_1"), + C3C.KnxGroupAddress("Day/Night", "0/1/0", "DPT_1"), + C3C.KnxGroupAddress("Date", "0/1/1", "DPT_11"), + C3C.KnxGroupAddress("Time", "0/1/2", "DPT_10"), + C3C.KnxGroupAddress("Led Intensity", "0/1/100", "DPT_5"), + C3C.KnxGroupAddress("Retention Tank Water Level Low", "0/1/200", "DPT_1"), + C3C.KnxGroupAddress("Scene Lower Floor", "0/2/100", "DPT_17"), + C3C.KnxGroupAddress("Scene Lower Floor Hallway", "0/2/101", "DPT_17"), + C3C.KnxGroupAddress("Scene Lower Floor Bathroom", "0/2/102", "DPT_17"), + C3C.KnxGroupAddress("Scene Lower Floor Laundry", "0/2/103", "DPT_17"), + C3C.KnxGroupAddress("Scene Lower Floor Bedroom 1", "0/2/104", "DPT_17"), + C3C.KnxGroupAddress("Scene Lower Floor Study", "0/2/105", "DPT_17"), + C3C.KnxGroupAddress("Scene Lower Floor Bedroom 2", "0/2/106", "DPT_17"), + C3C.KnxGroupAddress("Scene Lower Floor Technical Room", "0/2/107", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor", "0/2/150", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Hallway", "0/2/151", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Bathroom", "0/2/152", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Bedroom", "0/2/153", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Closet", "0/2/154", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Living Room", "0/2/155", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Office", "0/2/156", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Dining Room", "0/2/157", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Kitchen", "0/2/158", "DPT_17"), + C3C.KnxGroupAddress("Scene Upper Floor Pantry", "0/2/159", "DPT_17"), + C3C.KnxGroupAddress("Scene Staircase", "0/2/200", "DPT_17"), + C3C.KnxGroupAddress("Scene Swimming Pool", "0/2/210", "DPT_17"), + C3C.KnxGroupAddress("Scene Ventilation Valves", "0/2/220", "DPT_17"), + C3C.KnxGroupAddress("Scene Ventilation Work", "0/2/221", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway", "1/0/0", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway CTRL", "1/0/1", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway Light 1", "1/0/2", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway Light 2", "1/0/3", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway Light Below staircase", "1/0/4", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway FB", "1/0/7", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway CTRL FB", "1/0/8", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway Light 1 FB", "1/0/9", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway Light 2 FB", "1/0/10", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Hallway Light Below staircase FB", "1/0/11", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary", "1/0/20", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary CTRL", "1/0/21", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary Light 1", "1/0/22", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary Light 2", "1/0/23", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary Light Vanity", "1/0/24", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary Ceiling", "1/0/25", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary FB", "1/0/27", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary CTRL FB", "1/0/28", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary Light 1 FB", "1/0/29", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary Light 2 FB", "1/0/30", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Primary Light Vanity FB", "1/0/31", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient", "1/0/35", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient CTRL", "1/0/36", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient Light 1", "1/0/37", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient Light 2", "1/0/38", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient Toilet", "1/0/39", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient FB", "1/0/42", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient CTRL FB", "1/0/43", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient Light 1 FB", "1/0/44", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient Light 2 FB", "1/0/45", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bathroom Ambient Toilet FB", "1/0/46", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Laundry Primary", "1/0/70", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Laundry Primary CTRL", "1/0/71", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Laundry Primary Light 1", "1/0/72", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Laundry Primary Light 2", "1/0/73", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Laundry Primary FB", "1/0/77", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Laundry Primary CTRL FB", "1/0/78", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Laundry Primary Light 1 FB", "1/0/79", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Laundry Primary Light 2 FB", "1/0/80", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary", "1/0/90", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary CTRL", "1/0/91", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary Light 1", "1/0/92", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary Light 2", "1/0/93", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary Light Reading", "1/0/94", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary FB", "1/0/97", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary CTRL FB", "1/0/98", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary Light 1 FB", "1/0/99", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary Light 2 FB", "1/0/100", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Primary Light Reading FB", "1/0/101", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Ambient", "1/0/105", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Ambient CTRL", "1/0/106", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Ambient Light 1", "1/0/107", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Ambient Light 2", "1/0/108", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Ambient FB", "1/0/112", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Ambient CTRL FB", "1/0/113", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Ambient Light 1 FB", "1/0/114", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 1 Ambient Light 2 FB", "1/0/115", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Primary", "1/0/140", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Primary CTRL", "1/0/141", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Primary Light 1", "1/0/142", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Primary Light 2", "1/0/143", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Primary FB", "1/0/147", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Primary CTRL FB", "1/0/148", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Primary Light 1 FB", "1/0/149", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Primary Light 2 FB", "1/0/150", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Ambient", "1/0/155", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Ambient CTRL", "1/0/156", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Ambient Light 1", "1/0/157", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Ambient Light 2", "1/0/158", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Ambient FB", "1/0/162", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Ambient CTRL FB", "1/0/163", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Ambient Light 1 FB", "1/0/164", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Study Ambient Light 2 FB", "1/0/165", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary", "1/0/190", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary CTRL", "1/0/191", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary Light 1", "1/0/192", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary Light 2", "1/0/193", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary Light Reading", "1/0/194", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary FB", "1/0/197", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary CTRL FB", "1/0/198", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary Light 1 FB", "1/0/199", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary Light 2 FB", "1/0/200", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Primary Light Reading FB", "1/0/201", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Ambient", "1/0/205", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Ambient CTRL", "1/0/206", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Ambient Light 1", "1/0/207", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Ambient Light 2", "1/0/208", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Ambient FB", "1/0/212", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Ambient CTRL FB", "1/0/213", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Ambient Light 1 FB", "1/0/214", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Bedroom 2 Ambient Light 2 FB", "1/0/215", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Utility", "1/0/225", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Utility CTRL", "1/0/226", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Utility Light 1", "1/0/227", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Utility Counter", "1/0/228", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Utility FB", "1/0/232", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Utility CTRL FB", "1/0/233", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Utility Light 1 FB", "1/0/234", "DPT_1"), + C3C.KnxGroupAddress("Switch LF Utility Counter FB", "1/0/235", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Entry", "1/1/0", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Isle", "1/1/1", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway CTRL", "1/1/2", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Entry 1", "1/1/3", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Entry 2", "1/1/4", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Isle 1", "1/1/5", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Isle 2", "1/1/6", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Entry FB", "1/1/7", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Isle FB", "1/1/8", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway CTRL FB", "1/1/9", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Entry 1 FB", "1/1/10", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Entry 2 FB", "1/1/11", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Isle 1 FB", "1/1/12", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Hallway Isle 2 FB", "1/1/13", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary", "1/1/20", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary CTRL", "1/1/21", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary Light 1", "1/1/22", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary Light 2", "1/1/23", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary Light Vanity", "1/1/24", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary Ceiling", "1/1/25", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary FB", "1/1/27", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary CTRL FB", "1/1/28", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary Light 1 FB", "1/1/29", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary Light 2 FB", "1/1/30", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Primary Light Vanity FB", "1/1/31", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Vanity CTRL", "1/1/33", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Vanity CTRL FB", "1/1/34", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient", "1/1/35", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient CTRL", "1/1/36", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient Light 1", "1/1/37", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient Light 2", "1/1/38", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient Toilet", "1/1/39", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient FB", "1/1/42", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient CTRL FB", "1/1/43", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient Light 1 FB", "1/1/44", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient Light 2 FB", "1/1/45", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bathroom Ambient Toilet FB", "1/1/46", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary", "1/1/50", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary CTRL", "1/1/51", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary Light 1", "1/1/52", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary Light 2", "1/1/53", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary Reading 1", "1/1/54", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary Reading 2", "1/1/55", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary FB", "1/1/57", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary CTRL FB", "1/1/58", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary Light 1 FB", "1/1/59", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary Light 2 FB", "1/1/60", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary Reading 1 FB", "1/1/61", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Primary Reading 2 FB", "1/1/62", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Ambient", "1/1/65", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Ambient CTRL", "1/1/66", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Ambient Light 1", "1/1/67", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Ambient Light 2", "1/1/68", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Ambient FB", "1/1/72", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Ambient CTRL FB", "1/1/73", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Ambient Light 1 FB", "1/1/74", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Bedroom Ambient Light 2 FB", "1/1/75", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Closet", "1/1/80", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Closet CTRL", "1/1/81", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Closet Light 1", "1/1/82", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Closet Light 2", "1/1/83", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Closet FB", "1/1/87", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Closet CTRL FB", "1/1/88", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Closet Light 1 FB", "1/1/89", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Closet Light 2 FB", "1/1/90", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary", "1/1/95", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary CTRL", "1/1/96", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary Light 1", "1/1/97", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary Light 2", "1/1/98", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary Light 3", "1/1/99", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary FB", "1/1/102", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary CTRL FB", "1/1/103", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary Light 1 FB", "1/1/104", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary Light 2 FB", "1/1/105", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Primary Light 3 FB", "1/1/106", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Ambient", "1/1/110", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Ambient CTRL", "1/1/111", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Ambient Light 1", "1/1/112", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Ambient Light 2", "1/1/113", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Ambient FB", "1/1/117", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Ambient CTRL FB", "1/1/118", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Ambient Light 1 FB", "1/1/119", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Living room Ambient Light 2 FB", "1/1/120", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Office Primary", "1/1/125", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Office Primary CTRL", "1/1/126", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Office Primary Light 1", "1/1/127", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Office Primary Light 2", "1/1/128", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Office Primary FB", "1/1/132", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Office Primary CTRL FB", "1/1/133", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Office Primary Light 1 FB", "1/1/134", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Office Primary Light 2 FB", "1/1/135", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Dining room", "1/1/140", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Dining room CTRL", "1/1/141", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Dining room Light 1", "1/1/142", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Dining room Light 2", "1/1/143", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Dining room FB", "1/1/147", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Dining room CTRL FB", "1/1/148", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Dining room Light 1 FB", "1/1/149", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Dining room Light 2 FB", "1/1/150", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary", "1/1/155", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary CTRL", "1/1/156", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary Light 1", "1/1/157", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary Light 2", "1/1/158", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary Light 3", "1/1/159", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary FB", "1/1/162", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary CTRL FB", "1/1/163", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary Light 1 FB", "1/1/164", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary Light 2 FB", "1/1/165", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Primary Light 3 FB", "1/1/166", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient", "1/1/170", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient CTRL", "1/1/171", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient Light 1", "1/1/172", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient Light 2", "1/1/173", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient Light 3", "1/1/174", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient FB", "1/1/177", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient CTRL FB", "1/1/178", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient Light 1 FB", "1/1/179", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient Light 2 FB", "1/1/180", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Ambient Light 3 FB", "1/1/181", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter", "1/1/185", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter CTRL", "1/1/186", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter Light 1", "1/1/187", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter Light 2", "1/1/188", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter Light 3", "1/1/189", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter FB", "1/1/192", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter CTRL FB", "1/1/193", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter Light 1 FB", "1/1/194", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter Light 2 FB", "1/1/195", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Kitchen Counter Light 3 FB", "1/1/196", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Pantry", "1/1/200", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Pantry CTRL", "1/1/201", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Pantry Light 1", "1/1/202", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Pantry Light 2", "1/1/203", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Pantry FB", "1/1/207", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Pantry CTRL FB", "1/1/208", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Pantry Light 1 FB", "1/1/209", "DPT_1"), + C3C.KnxGroupAddress("Switch UF Pantry Light 2 FB", "1/1/210", "DPT_1"), + C3C.KnxGroupAddress("Switch Staircase", "1/2/0", "DPT_1"), + C3C.KnxGroupAddress("Switch Staircase CTRL", "1/2/1", "DPT_1"), + C3C.KnxGroupAddress("Switch Staircase Tree", "1/2/2", "DPT_1"), + C3C.KnxGroupAddress("Switch Staircase Handrail", "1/2/3", "DPT_1"), + C3C.KnxGroupAddress("Switch Staircase FB", "1/2/7", "DPT_1"), + C3C.KnxGroupAddress("Switch Staircase CTRL FB", "1/2/8", "DPT_1"), + C3C.KnxGroupAddress("Switch Staircase Tree FB", "1/2/9", "DPT_1"), + C3C.KnxGroupAddress("Switch Staircase Handrail FB", "1/2/10", "DPT_1"), + C3C.KnxGroupAddress("Switch EXT Terrace", "1/3/0", "DPT_1"), + C3C.KnxGroupAddress("Switch EXT Terrace FB", "1/3/7", "DPT_1"), + C3C.KnxGroupAddress("Switch EXT Entrance", "1/3/20", "DPT_1"), + C3C.KnxGroupAddress("Switch EXT Entrance FB", "1/3/27", "DPT_1"), + C3C.KnxGroupAddress("Switch Parking place", "1/4/0", "DPT_1"), + C3C.KnxGroupAddress("Switch Parking place FB", "1/4/7", "DPT_1"), + C3C.KnxGroupAddress("Switch Parking place AC Plug Right of Door", "1/4/10", "DPT_1"), + C3C.KnxGroupAddress("Switch Parking place AC Plug Right of Door State", "1/4/17", "DPT_1"), + C3C.KnxGroupAddress("Switch Underwater Light", "1/5/0", "DPT_1"), + C3C.KnxGroupAddress("Switch Underwater Light FB", "1/5/1", "DPT_1"), + C3C.KnxGroupAddress("Switch Filtration", "1/5/10", "DPT_1"), + C3C.KnxGroupAddress("Switch Filtration FB", "1/5/11", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Hallway", "2/0/0", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Hallway FB", "2/0/1", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Hallway Dimmer", "2/0/2", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Hallway Color", "2/0/5", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Hallway Color FB", "2/0/6", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Hallway Color Dimmer", "2/0/7", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Primary", "2/0/20", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Primary FB", "2/0/21", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Primary Dimmer", "2/0/22", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Primary Color", "2/0/25", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Primary Color FB", "2/0/26", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Primary Color Dimmer", "2/0/27", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Ambient", "2/0/35", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Ambient FB", "2/0/36", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Ambient Dimmer", "2/0/37", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Ambient HSV", "2/0/40", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Ambient HSV FB", "2/0/41", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Ambient Hue Dimming", "2/0/42", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Ambient Saturation Dimming", "2/0/43", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Vanity", "2/0/50", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Vanity FB", "2/0/51", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Vanity Dimmer", "2/0/52", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Vanity Color", "2/0/53", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Vanity Color FB", "2/0/54", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bathroom Vanity Color Dimmer", "2/0/55", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Laundry Primary", "2/0/70", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Laundry Primary FB", "2/0/71", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Laundry Primary Dimmer", "2/0/72", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Laundry Color", "2/0/75", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Laundry Color FB", "2/0/76", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Laundry Color Dimmer", "2/0/77", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Primary", "2/0/90", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Primary FB", "2/0/91", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Primary Dimmer", "2/0/92", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Primary Color", "2/0/95", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Primary Color FB", "2/0/96", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Primary Color Dimmer", "2/0/97", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Ambient", "2/0/105", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Ambient FB", "2/0/106", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Ambient Dimmer", "2/0/107", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Ambient HSV", "2/0/110", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Ambient HSV FB", "2/0/111", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Ambient Hue Dimming", "2/0/112", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 1 Ambient Saturation Dimming", "2/0/113", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Study Primary", "2/0/140", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Study Primary FB", "2/0/141", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Study Primary Dimmer", "2/0/142", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Study Color", "2/0/145", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Study Color FB", "2/0/146", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Study Color Dimmer", "2/0/147", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Study Ambient", "2/0/155", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Study Ambient FB", "2/0/156", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Study Ambient Dimmer", "2/0/157", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Study Ambient HSV", "2/0/160", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Study Ambient HSV FB", "2/0/161", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Study Ambient Hue Dimming", "2/0/162", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Study Ambient Saturation Dimming", "2/0/163", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Primary", "2/0/190", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Primary FB", "2/0/191", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Primary Dimmer", "2/0/192", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Primary Color", "2/0/195", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Primary Color FB", "2/0/196", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Primary Color Dimmer", "2/0/197", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Ambient", "2/0/205", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Ambient FB", "2/0/206", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Ambient Dimmer", "2/0/207", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Ambient HSV", "2/0/210", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Ambient HSV FB", "2/0/211", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Ambient Hue Dimming", "2/0/212", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Bedroom 2 Ambient Saturation Dimming", "2/0/213", "DPT_1"), + C3C.KnxGroupAddress("Dimmer LF Utility", "2/0/225", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Utility FB", "2/0/226", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Utility Dimmer", "2/0/227", "DPT_3"), + C3C.KnxGroupAddress("Dimmer LF Utility Color", "2/0/230", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Utility Color FB", "2/0/231", "DPT_5"), + C3C.KnxGroupAddress("Dimmer LF Utility Color Dimmer", "2/0/232", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Hallway", "2/1/0", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Hallway FB", "2/1/1", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Hallway Dimming", "2/1/2", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Hallway Color", "2/1/5", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Hallway Color FB", "2/1/6", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Hallway Color Dimming", "2/1/7", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Primary", "2/1/15", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Primary FB", "2/1/16", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Primary Dimming", "2/1/17", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Primary Color", "2/1/20", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Primary Color FB", "2/1/21", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Primary Color Dimming", "2/1/22", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Ambient", "2/1/30", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Ambient FB", "2/1/31", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Ambient Dimming", "2/1/32", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Ambient HSV", "2/1/35", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Ambient HSV FB", "2/1/36", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Ambient Hue Dimming", "2/1/37", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Ambient Saturation Dimming", "2/1/38", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Vanity", "2/1/45", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Vanity FB", "2/1/46", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Vanity Dimming", "2/1/47", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Vanity Color", "2/1/50", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Vanity Color FB", "2/1/51", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bathroom Vanity Color Dimming", "2/1/52", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Primary", "2/1/65", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Primary FB", "2/1/66", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Primary Dimming", "2/1/67", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Primary Color", "2/1/70", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Primary Color FB", "2/1/71", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Primary Color Dimming", "2/1/72", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Ambient", "2/1/80", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Ambient FB", "2/1/81", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Ambient Dimming", "2/1/82", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Ambient HSV", "2/1/85", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Ambient HSV FB", "2/1/86", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Ambient Hue Dimming", "2/1/87", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Bedroom Ambient Saturation Dimming", "2/1/88", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Closet", "2/1/95", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Closet FB", "2/1/96", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Closet Dimming", "2/1/97", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Closet Color", "2/1/100", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Closet Color FB", "2/1/101", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Closet Color Dimming", "2/1/102", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Living room Primary", "2/1/105", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Living room Primary FB", "2/1/106", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Living room Primary Dimming", "2/1/107", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Living room Primary Color", "2/1/110", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Living room Primary Color FB", "2/1/111", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Living room Primary Color Dimming", "2/1/112", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Living room Ambient", "2/1/120", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Living room Ambient FB", "2/1/121", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Living room Ambient Dimming", "2/1/122", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Living room Ambient HSV", "2/1/125", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Living room Ambient HSV FB", "2/1/126", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Living room Ambient Hue Dimming", "2/1/127", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Living room Ambient Saturation Dimming", "2/1/128", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Office Primary", "2/1/135", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Office Primary FB", "2/1/136", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Office Primary Dimming", "2/1/137", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Office Color", "2/1/140", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Office Color FB", "2/1/141", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Office Color Dimming", "2/1/142", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Dining room", "2/1/150", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Dining room FB", "2/1/151", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Dining room Dimming", "2/1/152", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Dining Room Color", "2/1/155", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Dining Room Color FB", "2/1/156", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Dining Room Color Dimming", "2/1/157", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Primary", "2/1/165", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Primary FB", "2/1/166", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Primary Dimming", "2/1/167", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Primary Color", "2/1/170", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Primary Color FB", "2/1/171", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Primary Color Dimming", "2/1/172", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Ambient", "2/1/180", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Ambient FB", "2/1/181", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Ambient Dimming", "2/1/182", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Ambient HSV", "2/1/185", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Ambient HSV FB", "2/1/186", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Ambient Hue Dimming", "2/1/187", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Ambient Saturation Dimming", "2/1/188", "DPT_1"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Counter", "2/1/195", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Counter FB", "2/1/196", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Counter Dimming", "2/1/197", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Counter Color", "2/1/200", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Counter Color FB", "2/1/201", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Kitchen Counter Color Dimming", "2/1/202", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Pantry", "2/1/210", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Pantry FB", "2/1/211", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Pantry Dimming", "2/1/212", "DPT_3"), + C3C.KnxGroupAddress("Dimmer UF Pantry Color", "2/1/215", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Pantry Color FB", "2/1/216", "DPT_5"), + C3C.KnxGroupAddress("Dimmer UF Pantry Color Dimming", "2/1/217", "DPT_3"), + C3C.KnxGroupAddress("Dimmer Staircase Tree Dimming", "2/2/0", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Staircase Tree Value", "2/2/1", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Staircase Tree Value Feedback", "2/2/2", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Staircase handrail Swtich", "2/2/20", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Staircase handrail Value", "2/2/21", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Staircase handrail Value Feedback", "2/2/22", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Staircase handrail bottom Dimming", "2/2/35", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Staircase handrail bottom Value", "2/2/36", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Staircase handrail bottom Value Feedback", "2/2/37", "DPT_1"), + C3C.KnxGroupAddress("Dimmer EXT Terrace Dimming", "2/3/0", "DPT_1"), + C3C.KnxGroupAddress("Dimmer EXT Terrace Value", "2/3/1", "DPT_1"), + C3C.KnxGroupAddress("Dimmer EXT Terrace Value Feedback", "2/3/2", "DPT_1"), + C3C.KnxGroupAddress("Dimmer EXT Entrance Value Feedback", "2/3/20", "DPT_1"), + C3C.KnxGroupAddress("Dimmer EXT Entrance Dimming", "2/3/21", "DPT_1"), + C3C.KnxGroupAddress("Dimmer EXT Entrance Value", "2/3/22", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Parking place Dimming", "2/4/0", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Parking place Value", "2/4/1", "DPT_1"), + C3C.KnxGroupAddress("Dimmer Parking place Value Feedback", "2/4/2", "DPT_1"), + C3C.KnxGroupAddress("Heat LF Hallway Temperature FB", "3/0/0", "DPT_9"), + C3C.KnxGroupAddress("Heat LF Bathroom Temperature FB", "3/0/30", "DPT_9"), + C3C.KnxGroupAddress("Heat LF Bathroom Ladder boost", "3/0/40", "DPT_1"), + C3C.KnxGroupAddress("Heat LF Bathroom Ladder boost FB", "3/0/41", "DPT_1"), + C3C.KnxGroupAddress("Heat LF Laundry Temperature FB", "3/0/60", "DPT_1"), + C3C.KnxGroupAddress("Heat LF Bedroom 1 Temperature FB", "3/0/90", "DPT_9"), + C3C.KnxGroupAddress("Heat LF Study Temperature FB", "3/0/120", "DPT_9"), + C3C.KnxGroupAddress("Heat LF Bedroom 2 Temperature FB", "3/0/150", "DPT_9"), + C3C.KnxGroupAddress("Heat LF Technical Room Temperature FB", "3/0/180", "DPT_9"), + C3C.KnxGroupAddress("Heat UF Hallway Temperature FB", "3/1/0", "DPT_9"), + C3C.KnxGroupAddress("Heat UF Bathroom Temperature FB", "3/1/30", "DPT_9"), + C3C.KnxGroupAddress("Heat UF Bathroom Ladder boost", "3/1/40", "DPT_1"), + C3C.KnxGroupAddress("Heat UF Bathroom Ladder boost FB", "3/1/41", "DPT_1"), + C3C.KnxGroupAddress("Heat UF Bedroom Temperature FB", "3/1/60", "DPT_9"), + C3C.KnxGroupAddress("Heat UF Living Room Temperature FB", "3/1/90", "DPT_9"), + C3C.KnxGroupAddress("Heat UF Kitchen Temperature FB", "3/1/120", "DPT_9"), + C3C.KnxGroupAddress("Heat Circulation Pump", "3/5/0", "DPT_1"), + C3C.KnxGroupAddress("Heat Circulation Pump FB", "3/5/1", "DPT_1"), + C3C.KnxGroupAddress("Heat Hot Water Circulation State", "3/6/2", "DPT_1"), + C3C.KnxGroupAddress("Heat Hot Water Circulation Emergency State", "3/6/3", "DPT_1"), + C3C.KnxGroupAddress("Heat Hot Water Circulation Control Value", "3/6/4", "DPT_5"), + C3C.KnxGroupAddress("Heat Heating-Cooling", "3/7/0", "DPT_1"), + C3C.KnxGroupAddress("Heat Heating-Cooling State", "3/7/1", "DPT_1"), + C3C.KnxGroupAddress("Heat Temperature Setpoint", "3/7/2", "DPT_1"), + C3C.KnxGroupAddress("Heat Temperature Setpoint State", "3/7/3", "DPT_1"), + C3C.KnxGroupAddress("Heat Heating Off lock", "3/7/4", "DPT_1"), + C3C.KnxGroupAddress("Heat UF+LF Heating Circulation Pump", "3/7/20", "DPT_1"), + C3C.KnxGroupAddress("Heat UF+LF Heating Circulation Pump State", "3/7/21", "DPT_1"), + C3C.KnxGroupAddress("Vent LF PLACEHOLDER Hallway", "4/0/0", "DPT_1"), + C3C.KnxGroupAddress("Vent LF Hallway Valve IN", "4/0/10", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Hallway Valve IN FB", "4/0/11", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Bathroom CO2 FB", "4/0/30", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Bathroom Humidity FB", "4/0/31", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Bathroom boost", "4/0/35", "DPT_1"), + C3C.KnxGroupAddress("Vent LF Bathroom boost FB", "4/0/36", "DPT_1"), + C3C.KnxGroupAddress("Vent LF Bathroom Valve IN", "4/0/40", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Bathroom Valve IN FB", "4/0/41", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Bathroom Valve OUT", "4/0/42", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Bathroom Valve OUT FB", "4/0/43", "DPT_5"), + C3C.KnxGroupAddress("Vent LF PLACEHOLDER Laundry", "4/0/60", "DPT_1"), + C3C.KnxGroupAddress("Vent LF Bedroom 1 CO2 FB", "4/0/90", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Bedroom 1 Humidity FB", "4/0/91", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Bedroom 1 Valve IN", "4/0/100", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Bedroom 1 Valve IN FB", "4/0/101", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Study CO2 FB", "4/0/120", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Study Humidity FB", "4/0/121", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Study Valve IN", "4/0/130", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Study Valve IN FB", "4/0/131", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Study Valve IN Lock", "4/0/132", "DPT_1"), + C3C.KnxGroupAddress("Vent LF Bedroom 2 CO2 FB", "4/0/150", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Bedroom 2 Humidity FB", "4/0/151", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Bedroom 2 Valve IN", "4/0/160", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Bedroom 2 Valve IN FB", "4/0/161", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Technical Room CO2 FB", "4/0/180", "DPT_1"), + C3C.KnxGroupAddress("Vent LF Technical Room Humidity FB", "4/0/181", "DPT_9"), + C3C.KnxGroupAddress("Vent LF Box Valve OUT", "4/0/240", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Box Valve OUT FB", "4/0/241", "DPT_5"), + C3C.KnxGroupAddress("Vent LF Box Valve OUT Lock", "4/0/242", "DPT_1"), + C3C.KnxGroupAddress("Vent UF PLACEHOLDER Hallway", "4/1/0", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Bathroom CO2 FB", "4/1/30", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Bathroom Humidity FB", "4/1/31", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Bathroom boost", "4/1/35", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Bathroom boost FB", "4/1/36", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Bathroom Valve IN", "4/1/40", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Bathroom Valve IN FB", "4/1/41", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Bathroom Valve IN Lock", "4/1/42", "DPT_1"), + C3C.KnxGroupAddress("Vent UF PLACEHOLDER Bedroom", "4/1/60", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Bedroom Valve IN", "4/1/70", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Bedroom Valve IN FB", "4/1/71", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Bedroom Valve IN Lock", "4/1/72", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Bedroom CO2 FB", "4/1/80", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Bedroom Humidity FB", "4/1/81", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Living Room CO2 FB", "4/1/90", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Living Room Humidity FB", "4/1/91", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Living Room Valve IN", "4/1/100", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Living Room Valve IN FB", "4/1/101", "DPT_5"), + C3C.KnxGroupAddress("Vent UF PLACEHOLDER Office", "4/1/120", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Office Valve IN", "4/1/130", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Office Valve IN FB", "4/1/131", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Bathroom Valve OUT", "4/1/142", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Bathroom Valve OUT FB", "4/1/143", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Bathroom Valve OUT Lock", "4/1/144", "DPT_1"), + C3C.KnxGroupAddress("Vent UF PLACEHOLDER Dining Room", "4/1/150", "DPT_1"), + C3C.KnxGroupAddress("Vent UF PLACEHOLDER Kitchen", "4/1/180", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Kitchen Valve OUT", "4/1/190", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Kitchen Valve OUT FB", "4/1/191", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Kitchen CO2 FB", "4/1/200", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Kitchen Humidity FB", "4/1/201", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Kitchen TVOC FB", "4/1/202", "DPT_9"), + C3C.KnxGroupAddress("Vent UF Kitchen boost", "4/1/205", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Kitchen boost FB", "4/1/206", "DPT_1"), + C3C.KnxGroupAddress("Vent UF Closet+Hallway Valve OUT", "4/1/240", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Closet+Hallway Valve OUT FB", "4/1/241", "DPT_5"), + C3C.KnxGroupAddress("Vent UF Closet+Hallway Valve OUT Lock", "4/1/242", "DPT_1"), + C3C.KnxGroupAddress("Vent Airflow", "4/7/0", "DPT_13"), + C3C.KnxGroupAddress("Vent Standby FB", "4/7/1", "DPT_1"), + C3C.KnxGroupAddress("Vent Temperature: Supply", "4/7/5", "DPT_9"), + C3C.KnxGroupAddress("Vent Temperature: Outdoor", "4/7/6", "DPT_9"), + C3C.KnxGroupAddress("Vent Temperature: Extract", "4/7/7", "DPT_9"), + C3C.KnxGroupAddress("Vent Temperature: Exhaust", "4/7/8", "DPT_9"), + C3C.KnxGroupAddress("Vent Humidity: Supply", "4/7/10", "DPT_9"), + C3C.KnxGroupAddress("Vent Humidity: Outdoor", "4/7/11", "DPT_9"), + C3C.KnxGroupAddress("Vent Humidity: Extract", "4/7/12", "DPT_9"), + C3C.KnxGroupAddress("Vent Humidity: Exhaust", "4/7/13", "DPT_9"), + C3C.KnxGroupAddress("Vent Filter: Remaining (h)", "4/7/25", "DPT_7"), + C3C.KnxGroupAddress("Vent Filter: Replace", "4/7/26", "DPT_1"), + C3C.KnxGroupAddress("Vent Heating-Cooling", "4/7/30", "DPT_1"), + C3C.KnxGroupAddress("Vent Heating-Cooling State", "4/7/31", "DPT_1"), + C3C.KnxGroupAddress("Vent Boost", "4/7/100", "DPT_1"), + C3C.KnxGroupAddress("Vent Boost FB", "4/7/101", "DPT_1"), + C3C.KnxGroupAddress("Vent Auto mode", "4/7/102", "DPT_1"), + C3C.KnxGroupAddress("Vent Auto mode FB", "4/7/103", "DPT_1"), + C3C.KnxGroupAddress("Vent Preset", "4/7/104", "DPT_5"), + C3C.KnxGroupAddress("Vent Preset FB", "4/7/105", "DPT_5"), + C3C.KnxGroupAddress("Vent Preset Away", "4/7/106", "DPT_1"), + C3C.KnxGroupAddress("Vent Preset Away FB", "4/7/107", "DPT_1"), + C3C.KnxGroupAddress("Vent Away function", "4/7/108", "DPT_1"), + C3C.KnxGroupAddress("Vent Away function FB", "4/7/109", "DPT_1"), + C3C.KnxGroupAddress("Vent Temperature profile mode", "4/7/110", "DPT_5"), + C3C.KnxGroupAddress("Vent Temperature profile mode FB", "4/7/111", "DPT_5"), + C3C.KnxGroupAddress("Vent Temperature profile", "4/7/112", "DPT_5"), + C3C.KnxGroupAddress("Vent Temperature profile FB", "4/7/113", "DPT_5"), + C3C.KnxGroupAddress("Vent Boost Time", "4/7/114", "DPT_7"), + C3C.KnxGroupAddress("Vent Boost Time FB", "4/7/115", "DPT_7"), + C3C.KnxGroupAddress("Vent External setpoint", "4/7/116", "DPT_9"), + C3C.KnxGroupAddress("Vent External setpoint FB", "4/7/117", "DPT_9"), + C3C.KnxGroupAddress("Vent Error", "4/7/200", "DPT_1"), + C3C.KnxGroupAddress("Vent Status", "4/7/201", "DPT_5"), + C3C.KnxGroupAddress("Media UF Bathroom Playing", "5/1/100", "DPT_5"), + C3C.KnxGroupAddress("Media UF Bathroom Off", "5/1/101", "DPT_1"), + C3C.KnxGroupAddress("Media UF Bathroom Volume Down", "5/1/103", "DPT_1"), + C3C.KnxGroupAddress("Media UF Bathroom Volume Up", "5/1/104", "DPT_1"), + C3C.KnxGroupAddress("Media UF Bathroom Previous", "5/1/105", "DPT_1"), + C3C.KnxGroupAddress("Media UF Bathroom Next", "5/1/106", "DPT_1"), + C3C.KnxGroupAddress("Media UF Bathroom Preset Cycle Forward", "5/1/107", "DPT_1"), + C3C.KnxGroupAddress("Control LF Hallway Presence", "6/0/0", "DPT_1"), + C3C.KnxGroupAddress("Control LF Hallway Presence Enable", "6/0/1", "DPT_1"), + C3C.KnxGroupAddress("Control LF Hallway Presence Enable Workaround", "6/0/2", "DPT_3"), + C3C.KnxGroupAddress("Control LF Hallway Presence Alarm", "6/0/10", "DPT_1"), + C3C.KnxGroupAddress("Control LF Hallway Presence Alarm State", "6/0/11", "DPT_1"), + C3C.KnxGroupAddress("Control LF Study LED Brightness Night", "6/0/146", "DPT_5"), + C3C.KnxGroupAddress("Control UF Bathroom Presence", "6/1/30", "DPT_1"), + C3C.KnxGroupAddress("Control UF Bathroom Presence Enable", "6/1/31", "DPT_1"), + C3C.KnxGroupAddress("Control UF Bathroom Presence Delayed", "6/1/32", "DPT_1"), + C3C.KnxGroupAddress("Control UF Bathroom Brightness", "6/1/35", "DPT_9"), + } +end + \ No newline at end of file diff --git a/src/driver.lua b/src/driver.lua new file mode 100644 index 0000000..9d71743 --- /dev/null +++ b/src/driver.lua @@ -0,0 +1,52 @@ +local VERSION = "2025.10.0" + +local ID_KNX = 1 + +require("control4-utils.base") +require("control4-utils.hooks") +require("control4-utils.helpers") +require("control4-utils.knx") +C3C.KnxProxy.Setup(ID_KNX) +C3C.RemoteLogger.Setup("c4c-knx-presence") +C3C.Logger.DisableRemoteLogging() +-- C4:AllowExecute(false) +C3C.Lib = {} + +require("lib.createContext") +require("lib.state.index") +require("lib.presence.index") + +Driver = (function() + local class = { + initialized = false, + online = false, + } + + ---@param strCommand string + ---@param tParams table + function class:Execute(strCommand, tParams) + if strCommand == "ONLINE" then + class.online = true + elseif strCommand == "OFFLINE" then + class.online = false + end + end + + function class:SetInitialized() + C3C.RemoteLogger.Info("C3C KNX Presence driver initialized", { version = VERSION }) + class.initialized = true + end + + return class +end)() + +C3C.HookIntoOnDriverLateInit(function() + C3C.OneShotTimer.Add(5000, Driver.SetInitialized, "Initialized") +end) + +C3C.HookIntoExecuteCommand(function(strCommand, tParams) + tParams = tParams or {} + strCommand = strCommand or "" + + Driver:Execute(strCommand, tParams) +end) diff --git a/src/lib/createContext.lua b/src/lib/createContext.lua new file mode 100644 index 0000000..b2bc4bd --- /dev/null +++ b/src/lib/createContext.lua @@ -0,0 +1,14 @@ +--- @param ga GroupAddress +--- @return table +CreateGroupAddressContext = function(ga) + return { + trace = ga.Name .. "-" .. string.format("%d", C4:GetTime()) + } +end + +CreateRoomContext = function(location) + return { + location = location, + time = os.clock(), + } +end diff --git a/src/lib/presence/LF/Hallway.lua b/src/lib/presence/LF/Hallway.lua new file mode 100644 index 0000000..6e0928a --- /dev/null +++ b/src/lib/presence/LF/Hallway.lua @@ -0,0 +1,27 @@ +(function() + local createContext = function(name) + local ctx = CreateRoomContext("LF Hallway") + ctx.name = name + return ctx + end + + C3C.KnxAddresses.Watch("Control LF Hallway Presence", function(ga, state) + local ctx = createContext("GA: " .. ga.Name) + ctx.newVal = state.newVal + C3C.Logger.Debug("Watched Presence", ctx) + + if state.newVal == 1 then + time = os.date("*t") + if C3C.Lib.State.DayTime.IsDay() or (time.hour >= 7 and time.hour <= 22) then + C3C.Logger.Debug("Switching on primary light", ctx) + C3C.KnxAddresses.Get("Switch LF Hallway CTRL"):Send(1) + else + C3C.Logger.Debug("Switching on primary light with dimmed control during nighttime", ctx) + C3C.KnxAddresses.Get("Dimmer LF Hallway"):Send(2) + end + else + C3C.Logger.Debug("Presence ended", ctx) + C3C.KnxAddresses.Get("Switch LF Hallway CTRL"):Send(0) + end + end) +end)() diff --git a/src/lib/presence/LF/index.lua b/src/lib/presence/LF/index.lua new file mode 100644 index 0000000..f685a96 --- /dev/null +++ b/src/lib/presence/LF/index.lua @@ -0,0 +1 @@ +require("lib.presence.LF.Hallway") diff --git a/src/lib/presence/UF/Bathroom.lua b/src/lib/presence/UF/Bathroom.lua new file mode 100644 index 0000000..e0fee69 --- /dev/null +++ b/src/lib/presence/UF/Bathroom.lua @@ -0,0 +1,191 @@ +(function() + local disable = false + local present = false + + local mediaPlaying = false + local mediaOverridden = false + local lightOverridden = false + + C3C.KnxAddresses.Watch("Switch UF Bathroom Primary CTRL FB") + C3C.KnxAddresses.Watch("Switch UF Bathroom Ambient CTRL FB") + local gaPrimaryLightCTRLFB = C3C.KnxAddresses.Get("Switch UF Bathroom Primary CTRL FB") + local gaAmbientLightCTRLFB = C3C.KnxAddresses.Get("Switch UF Bathroom Ambient CTRL FB") + + local createContext = function(name) + local ctx = CreateRoomContext("UF Bathroom") + ctx.mediaOverridden = mediaOverridden + ctx.lightOverridden = lightOverridden + ctx.name = name + return ctx + end + + local overrideMedia = function(ga) + local ctx = createContext("overrideMedia") + ctx.ga = ga.Name + C3C.Logger.Debug("Overriding media", ctx) + mediaOverridden = true + C3C.OneShotTimer.Add(120 * 1000, function() + ctx.mediaPlaying = mediaPlaying + ctx.present = present + C3C.Logger.Debug("Checking media override reset after 120 seconds", ctx) + if not mediaPlaying and not present then + C3C.Logger.Debug("Resetting media override after 120 seconds", ctx) + mediaOverridden = false + end + end, "checkMediaOverride") + end + + local overrideLight = function(ga) + local ctx = createContext("overrideLight") + ctx.ga = ga.Name + C3C.Logger.Debug("Overriding light", ctx) + lightOverridden = true + C3C.OneShotTimer.Add(120 * 1000, function() + ctx.present = present + ctx.gaPrimaryLightCTRLFB = gaPrimaryLightCTRLFB.Value + ctx.gaAmbientLightCTRLFB = gaAmbientLightCTRLFB.Value + C3C.Logger.Debug("Checking light override reset after 120 seconds", ctx) + if not present and gaPrimaryLightCTRLFB.Value ~= 1 and gaAmbientLightCTRLFB.Value ~= 1 then + C3C.Logger.Debug("Resetting light override after 120 seconds", ctx) + lightOverridden = false + end + end, "checkLightOverride") + end + + local unOverrideMedia = function() + local ctx = createContext("unOverrideMedia") + C3C.Logger.Debug("Resetting media override", ctx) + mediaOverridden = false + end + + local unOverrideLight = function() + local ctx = createContext("unOverrideLight") + C3C.Logger.Debug("Resetting light override", ctx) + lightOverridden = false + end + + C3C.KnxAddresses.Watch("Media UF Bathroom Playing", function(ga) + mediaPlaying = ga.Value == 1 + end) + + C3C.KnxAddresses.Watch("Media UF Bathroom Off", overrideMedia) + C3C.KnxAddresses.Watch("Media UF Bathroom Preset Cycle Forward", overrideMedia) + C3C.KnxAddresses.Watch("Media UF Bathroom Volume Up", overrideMedia) + C3C.KnxAddresses.Watch("Media UF Bathroom Volume Down", overrideMedia) + C3C.KnxAddresses.Watch("Media UF Bathroom Previous", overrideMedia) + C3C.KnxAddresses.Watch("Media UF Bathroom Next", overrideMedia) + + C3C.KnxAddresses.Watch("Switch UF Bathroom Primary", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Primary CTRL", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Primary Light 1", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Primary Light 2", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Primary Light Vanity", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Primary Ceiling", overrideLight) + C3C.KnxAddresses.Watch("Dimmer UF Bathroom Primary", overrideLight) + C3C.KnxAddresses.Watch("Dimmer UF Bathroom Primary Dimming", overrideLight) + + C3C.KnxAddresses.Watch("Switch UF Bathroom Ambient", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Ambient CTRL", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Ambient Light 1", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Ambient Light 2", overrideLight) + C3C.KnxAddresses.Watch("Switch UF Bathroom Ambient Toilet", overrideLight) + C3C.KnxAddresses.Watch("Dimmer UF Bathroom Ambient", overrideLight) + C3C.KnxAddresses.Watch("Dimmer UF Bathroom Ambient Dimming", overrideLight) + + C3C.KnxAddresses.Watch("UF Bathroom Exit", function() + unOverrideMedia() + unOverrideLight() + end) + + + local presenceStart = function() + present = true + + local ctx = createContext("presenceStart") + C3C.Logger.Debug("Triggered: Presence start", ctx) + + if disable then + C3C.Logger.Debug("Presence start disabled", ctx) + return + end + + time = os.date("*t") + if C3C.Lib.State.DayTime.IsDay() or (time.hour >= 7 and time.hour <= 19) then + C3C.Logger.Debug("Switching on primary light", ctx) + C3C.KnxAddresses.Get("Switch UF Bathroom Primary CTRL"):Send(1) + elseif time.hour > 19 and time.hour <= 20 then + C3C.Logger.Debug("Switching on primary light with dimmed control during nighttime", ctx) + C3C.KnxAddresses.Get("Dimmer UF Bathroom Primary"):Send(12) + end + + + C3C.Logger.Debug("Firing event: UpperFloorBathroomMediaStart", ctx) + C4:FireEvent("UpperFloorBathroomMediaStart") + end + + local presenceEnd = function() + present = false + + local ctx = createContext("presenceEnd") + C3C.Logger.Debug("Triggered: Presence end", ctx) + + if not lightOverridden then + if gaPrimaryLightCTRLFB.Value ~ 0 then + C3C.Logger.Debug("Switching off primary light", ctx) + C3C.KnxAddresses.Get("Switch UF Bathroom Primary CTRL"):Send(0) + end + + if gaAmbientLightCTRLFB.Value ~= 0 then + C3C.Logger.Debug("Switching off ambient light", ctx) + C3C.KnxAddresses.Get("Switch UF Bathroom Ambient CTRL"):Send(0) + end + end + + if not mediaOverridden then + C3C.Logger.Debug("Firing event: UpperFloorBathroomMediaEnd", ctx) + C4:FireEvent("UpperFloorBathroomMediaEnd") + end + + unOverrideMedia() + unOverrideLight() + end + + local presenceEndDelayed = function() + local ctx = createContext("presenceEndDelayed") + C3C.Logger.Debug("Triggered: Presence end delayed", ctx) + + C3C.Logger.Debug("Firing event: UpperFloorBathroomMediaEnd", ctx) + C4:FireEvent("UpperFloorBathroomMediaEnd") + + if gaPrimaryLightCTRLFB.Value ~= 0 then + C3C.Logger.Debug("Switching off primary light", ctx) + C3C.KnxAddresses.Get("Switch UF Bathroom Primary CTRL"):Send(0) + end + if gaAmbientLightCTRLFB.Value ~= 0 then + C3C.Logger.Debug("Switching off ambient light", ctx) + C3C.KnxAddresses.Get("Switch UF Bathroom Ambient CTRL"):Send(0) + end + end + + C3C.KnxAddresses.Watch("Control UF Bathroom Presence", function(ga, state) + local ctx = createContext("GA: " .. ga.Name) + ctx.newVal = state.newVal + C3C.Logger.Debug("Watched GA Bathroom Presence", ctx) + if state.newVal == 1 then + presenceStart() + else + presenceEnd() + end + end) + + C3C.KnxAddresses.Watch("Control UF Bathroom Presence Delayed", function(ga, state) + local ctx = createContext("GA: " .. ga.Name) + ctx.newVal = state.newVal + C3C.Logger.Debug("Watched GA Bathroom Presence Delayed", ctx) + if state.newVal == 1 then + + else + presenceEndDelayed() + end + end) +end)() diff --git a/src/lib/presence/UF/index.lua b/src/lib/presence/UF/index.lua new file mode 100644 index 0000000..1143e0d --- /dev/null +++ b/src/lib/presence/UF/index.lua @@ -0,0 +1 @@ +require("lib.presence.UF.Bathroom") diff --git a/src/lib/presence/index.lua b/src/lib/presence/index.lua new file mode 100644 index 0000000..fc12bae --- /dev/null +++ b/src/lib/presence/index.lua @@ -0,0 +1,2 @@ +require("lib.presence.LF.index") +require("lib.presence.UF.index") diff --git a/src/lib/state/daytime.lua b/src/lib/state/daytime.lua new file mode 100644 index 0000000..ec27a1a --- /dev/null +++ b/src/lib/state/daytime.lua @@ -0,0 +1,13 @@ +(function() + C3C.Lib.State.DayTime = {} + + local isDay = false + + C3C.KnxAddresses.Watch("Day/Night", function(ga) + isDay = ga.Value == 0 + end) + + C3C.Lib.State.DayTime.IsDay = function() + return isDay + end +end)() diff --git a/src/lib/state/index.lua b/src/lib/state/index.lua new file mode 100644 index 0000000..4bce271 --- /dev/null +++ b/src/lib/state/index.lua @@ -0,0 +1,2 @@ +C3C.Lib.State = {} +require("lib.state.daytime") diff --git a/src/www/documentation.html b/src/www/documentation.html new file mode 100644 index 0000000..9cfa50c --- /dev/null +++ b/src/www/documentation.html @@ -0,0 +1,15 @@ + + + + Control4 KNX Ventilation + + + + + +

Control4 KNX Presence

+ +

Control4 driver managing the presence system

+ + + diff --git a/src/www/icons/logo.png b/src/www/icons/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8893db644b4eefd290b65da43b684d1b88052e9a GIT binary patch literal 88318 zcmV)~KzhH4P)EX>4Tx04R}tkv&MmKpe$iTcuJe4ptC#$WWc^QbinV6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfc5qU3krMxx6k5c1aNLh~_a1le0DrT}RI?`msG4PD zQb{3~UloF{2w@aMh(nN=sn3aG8lL0p9zMR_#dwzYxj#q0QZO0d6NnQ`H!R`};+aiL z=e$oGVP#1nJ|~_u=z_$LT$f#b<6Lss&od)NHZxBgAr?wqEO#+08!GWMaa2(?%J=77 zRyc2QR;zW^z9)ZSxS*{pbDicGQdqkM+H?_h|#K%Vj@HPNe};s;}^*#ldA$o zjs?`9LUR1zfAG6ovp5xZlfp@$_r}G}A}vQmG*yY?{jVqnG+x+41pjd3}KWRM2RQ} z%A|rr+oIiqf;R2Y0;`NHpGQ#~0eT+nWBaaaP{3ip1PM={Uf>?X+-w1a*0)Qir z<%ne0)~L4g!z}u<8tE9x*ZI!^R8$qw_Uqyo2~4Z%ASvF{>bht3*B#N9jsW0l0j*_S zbc@=goeCpsO2pH?>6Ybrw8Qrn7ZES%HCUZd^zyEW?uCbebRi@x0j}ZVId91XEuLexg2F%hHwd&HPX=nrr8UAMr5cJ+-2!9S2 zH7_0v+c`{Gax$8xcRSAM49N-85z9LiXMQ^lajdVI!ZB=4{V%SaQTL3adf{HQi?_Gz0 zMSJ9^;RnLA#%NE<|5@59xfwRSUKy!w`FU0`2RIS|jsW0l<7rmcVpG+Y)KQk|W0)lX zMURLLe@1MpgGh1ROznBX_Z|4~|G9J3vw1(Xwa@zicB&97z@~OxO_Go#35dlQ$eb<~ zWWsA0YB;)xsCva5PQ`UvU6I$~%rizFBLKMA^kI!yK;mPpV_G7Z0&uDg7v8Ja8W>0w ziHZ7_-xsm19F+))v}o>(F?gTH-x9n!tO{Opk|aUZF4{2(7 z;)y4C^zkQo>X~PG?5U?&IM~Es@XjMPZPVV_miw4U_}oSW=Q6NKJTN@Q8Z?hQqOoO^ zsj3*HCB0WwthLp3F$vB&j4`e2C*ydHs8Abr{pBw;} z6}X1eu`TH-zEaYTVnawhbu~luaT)0h51{4- zF$&fSF3U0Earpq&2trOzBW2vgaZH&qh3T7Y!X}$+!Uj{;XVN+on7q!qOq@88Q9Zr% z^mdaZ35^7diC-E%S`l|jE&epM#MTs%Cg43d1t;*AJO9oHk2{`+9+`(RDOr|d(xm0H z1pp5KtzHpT6bG6ks-#K6K$eGS(t>)xyYTwnhdkd%%VAR%UyMOjan23R8d?q^>C7LX zOBi*p!0e!{C^G_t9pa6D21n=>4S-AfT6=btqYb7qdGcgNjq1S)SQ|jf zm`asmVp{2a_4gW%BQ>y71u*um*+-zTF6WKd@E;xnU;-WB1kx5RDx~!X^RDg zrVD8*DtJQLhWaup67NWC06uj&CT7eAW!%_t%-DQ0c6s&AY`N78wx2bN4W_P7cO&p| zCWdH9@p0=CJgLC1$il<>l<<4LRz0pGpAn2vh@eZ1XpLw~u|-v}lF*YH)}1hp4}IYM zobaj7gmlvc@CqM9LYnH8Mgp^DzKWASeG)tExD81XqPK+$7xMVyPjdU5JGuF$+xW{J zb9nU8M-hCCn3E8rsmF@JmGl7Qd4`B>seFg~?}tzos~dMlfbSI+0j?HhEVkcn7MpCm z5fjIcBS`{?6}Tm_K2ZRm#HdIV#2CC*sbM|p5K&YEI4rKnBoW4|5HduBLIpC40kI6^ zaNj>3W%dDYW-y1rJf!muA@>edM|UF3+HwY${Ny6KtkBylxIDucgVDfM<{lKGe=z5< zC!XTwTW{fx+vjlGZMXB#!*dzT@_^eW@P;0Ve;Q2u9x5olW>9EaBI@=7O~u!^z@Z1- z%2>-Ig^_;(09@1tTHX|NSwV1-8+wHTU@H&U`PTz+UtR|jxGq(P&zo3*bta8x*PVCb zwfpSNp1beDhEu1ISfOD9u*EV3uj9s>Zec-RKg}!;hYSXf#33Y>+L*0|7Gr& z^H=_GfrnVwH;73B7+Sy?5H;0DH@1z3GiVF&#Y1l;gdLsMVx0#bd5my? zDCVQ>3sb<8W?owwkab(LsbNcPb@`pECSv|uu5<- zEjKm-Vro$pTzt=|F?{d?@5e^I(v*B@9MO#Fi0N5CuypXt*4~qb|cahZPt*rkA~T+m-!hzmC`Jy%$ra ztV20+5$vR7tN8OBa~K%RLs~81#ekZS{%TyGqO|A$btE!d)WP|RiU1e&!H07N=>(E% zBhOJ(X#uRY>`j9G@XQP zFOf$RjUwGeq^TVbpe4VFWj9C11F6$}T!f)PfjFuf3oapId*bP*nK$<_JQlB>+~wtS zPMW)~~>J+%%#jvb5 zYgI*fi~9efl3D;_YAh~ql32lrM{sPq;f5T3*ulL0@VBz@M(c#gOv4Q{ai>a6yJncwc+9{4&>Hmt&>MWs{?`D zPf!}EWy*S!_|$)Yk~hwN9bFAV@2XR*l?Z-P^^?`)DAamlJEpWPG7Zxf?J#UwxTsH1 z%WyjsK|=ah6<6BY^T^`&V+2{gJsYAVV9kNFCskq>Za1on09xo=30} zBrT{T!LiJdc}s7qA@>|H5YRY+tb})^o7czBgi4o zXNJw8HKydgcH6wyn$?vr4Gl`*HG=aY?VWSa-#O;kkMQ`@&oPiWoLY<|V1ztxh7OoK zV^mMdAFlZon{BjC&?jps{uHh`fdh;{>}`ntqz!-y05n6n%z6h%Nm)pn3949fguZ4@ zU*_3$kJmAO;UHNKYAjCk`1-A;7O!*=Y##-?s)8D`rUS&1aDZ#W30J(=Rzm|peCXQ^ zv606J{MW}n#;Z2lgoc4spvwxr)`V|u;KjTuVgg^Lnj&03dBfmv`o?!Q`jOy)T*PV9?PAWq1)$_sDDd^>@ z=%eV`D1G|MJhX}ki|CN$*i`iyAd0laei|b?m&0?`1kn5f4#QhFNQvExJ-7#N_huaD=RpU-nIzJxc19E+2r z>^?z5*NW3%Uf|Rr)^OSxXR^yKJF)AI+mOd1QCXZR+!bi0KSN4N^$Dd^B39S1tYX(V zOl!jLB1jBzi+gE}p}Uc=`DUAP-3>RA_KYSg0MCbNyc!w8!!C*H;g!Mzt_oe_h$Hl3 z@64jzTAT*iiov3}hD8lvG-~KE>Iw z?hk+D=38&!!3Q6tuWx{XW{$<+qfcNVZZ7z<;<-c7!{!(5? zlj8BEk6`#ervX0G=rNdxL2+eQPyy~%!gaIp zzIv(FUGS5YQmEX4Qzg*E#@PQ>?#RM^{R0 zo)3AVSc90LMH3G`MrSJEU5H!)52UEeYi6FI8eHlEO<3*0vdNdJj+ z9SbhXg=b%^M2bgUj+G?7b`@D#%^BUBa``0}Gi&B1;4%^^B00e%7LUU@M`|0T{d|aG zil+Nk&#FScQKi7)=Yn5gZL(Vw2#TFQ=DHhDpCBf| zOJ#~s==y+Y-z9AoVU5?$mDixb;8!Bsp)=t!80E=hZ22(lW zZHMxf0}o*OCL0F}1n z;i0*Yq9mbHM2iDSu@ZjXilaMC*?H%ku%g^^-`{!c$){*~CGF}Z&ppP+&JqMQZJ9({ z@oMpfuQlgU=Rrjztfvtj;EFAlEY)+CO#-G$nOa(xOo@~doBF-?Ws2n0ktRv({|o}) zK(yBZ$>4H_wFw#xK#ehoP3h|&;I=#NV9ObskeCGLa;!+LV??oHN?R(^F;%EXep)p{ zKv_&F1igVP1gF748Jpl^D_)i>3;X-|zSK!;rG7F)OE*|o{JEw*qMhv-FiEp``<5e`yF?Y zCJmf7U=wnRf=h83Wyt*GdWKV8eQD(eMa{68)T&5Br6?T9>e2JA@n#5%;{lgD1*Wyg z;bqavpgI5+8{5ZJPF$13D!c5wBX8VqHWMaH;Klh1x%T?&x#3SYk*N;^wvTd@$RCPG z$-yNAE#No4atd!fbbnF}@6j3)oSci((dPjW%R-`AY`2U^C}NbIX)$BX6#51n_doCu z*Z=X4{PMCZ`0JhbFwk_^G$Bd5@W$fMDg{;3lJXbtzZC$h10?*s?*L#!0k8nn;&72+ z=WNtAsaM2C>nRglZvrh_6mv8`a&m2i&BL^6wJ0~!h!DG)n$C2;(7w*0PA9yu*NiepnlrZ5PUE&!(riW{; z`UM+LSr-9p8vr7Z0fSA?CI5Fhr+o1%Jp24hc(GuTU;^s}A0;{UgQ6Vnua=h<6Vry=vq&8%F8Cfw%^_M_eGn^LGc6ITbl|p%6gWh}F&cBetib2q zh019Zndf;PjR-=TZ%ml&5rT3L>cVGGPfyxl^12h)a>f?yvHNc9v)7)?oUu7+Dy51> zB-m{=U8s}fkxzZO-*d^OmvicAr}OC3PvcQ?A39Rh3)ZH1Lgbfd#^*nC5=X!Hh^Ro6 zXnaw#8k4}kia_q*xffsNtZ$#k1s7h#!u~-_+QnezP)v=(ZaWAI|H#Niv~%5}?FGHX z2Nx?EpjM|D!&(^uu1Wxu+OQ!lIu>F#ffYqulMU8ikK;db9B)7D5JvX~_D|GfV^!8E z3_5u7nHM&j3}vzfZ!a~_%a1I zQ6^KE!1#KQWPm%%A@fXlOY@Y~b7y8x0CVs^^L- zX**E4`ZTuILn~A{TWz*XIU2;aY_d)OSg&SFl(2|*2SHm!6^1960W^1z>{V>I;Z!D0oJf)+3^eZ+@G=046F z4myPS3kUHgZFODtRoY_JqrQpv18h0tRqQwW_3X0CtJ!qgG{%k|MVbVEWA6hXWLd^Q zvq}Ggex95E0`newoJZ!)<=*@6=f3+N3tjX^OQe_=?*qc$iT6lv1Zx zPp=9?E%k+MlrFa7*g55;1TU?1NT6XemjH-jH0u2FCYw&%n5k2z;+KE0WS$D*$kD9K$MoFGa*r zED-T!Q9)t_w*WIykJX$;Vi-GSG@t#{i5&Lk0~p;E(%rx@xsdWna5j;0&adFSVG#K7 z&wt7B|Me+I8i6}1x)0P~)Rb}x(F`QV`DVFE6Dy4B?xDA*i-t|e^I$YEm}NAZO$G*< zq2t2)ka`Iin}A6|8Yl)e64FAGqPZ(aO9CW~GysGqp$b}T5UGj`0jwzjYHD{iKmX!7G83->CnOCKG*`ZLG9 z5B!7w{I3(Z>u>jwdB7w%Z;&JJoxsiB>5oJtw!u7xVEgMc5A9P#Aj9~@5CrM0M zH1P2(a|$oM_#!X7@B$A!@DO+Y^=@vz^G@!$_dcF_<^^)+Ll3hF5)GNRS|Lu{eA-$kFeAFDHEbqm1e{Btp=VMO-kEJ5trarPZpQVWSHcgB3XNfH(53 zv(LjclD7F8Rg0SU8qGij@1>lZ;Z#G+kSfmmk{8QF0E_b#9~53TAg73u8XG^Kqp$(+ z6As#cUjX_Blz%)jmz!_7jX(V9CT_j$cAk3rnNS?j zFc?bXVdZLiO0QbFOD7Uqw|ptsuBd_%Y+`@0(Lxhh95kVWWz?uqm9%D2F5Ml#p*Rc$ zrD23|qq;fvn15&Y-FN3>pFEKV9-bHOxe=OKjz*d%aLuii`o$GGR+T1ib@70!a(uS) z001BWNklCE0|FS=}KQ?nAh=EAR; zn86r~t#=F*uVc&fF0ruVj@y^WskSyKsKCHt1}}nlQ7`DD(I*m8Tw-L9CM|l{+`E!c zH&NJYjltzjG|zEBni$qwcM_XTo5mKKO=sTYkMV~;-aw`y63o%^6~2SPX3Erh zq=!jt+dFFNL(!>&s)=GmYS%w@W{8lxoc;Ej9nD|CWqF7g~PT%`a`N) zc5#F^G`3sVc8h6jJ98TU`p&}`%#^>(xs$7}zJ_bAyPk(0ew2YM56>fpyf$3EJRH^G z)REW*K93c56FOW9s=`E_Q4ziQc&-`s^er4%zb?TB$EUS0sGiRG2W%qFSNeEw%NXh>Ei;4RAj zdiCYMQSfRF&V`dZ=A;C$t_EWmA2U;#{i?CA3!srxl}u%Lam0(Hb^be|`2p;0^4v z_g-wj?KZ4Cacppz7I=8x6C88gaolz9{p8-@jYA|w2^+eoMiw11g?QR5#VrhLpo$#B zzdL9Fqx{CHXUv!}SQ~Y6Hd@QO91~}Lr~!4Zx>hF0Y}DoD*iVTjsD=5e_d=I}q+8f? z*KOHjx9vFbgpYE^oWF7TRlniazrBWs9)1LzC(k|Brop*e6=Ojp7#N5ttY)GGnQ0YH zBOY&j*)=2@9IWR)@+dQ>ZxpVbwAL#+U9+kqCAR1pTMl|M>SGJFVZxYR{^!#tvgxLq za>|##h7|+WhVGO+$H&7Xj5O7HZDft3q(*vyS6K=iII|>4@VNuYvEI|28csgxlf3Ue zN6?dmbTTbgotGG_#PQgX0JLIi2pVkf^&X`7sR0oqS{ox^f&Ybfp{Sl&pq@Fmo`m!m)QA3XN>Gk6TfrlEZ?u9Y<@6+HyhWaL(ZnObsee)aavcuM-5`ZsMg0*y2@kDt>OeqSUC{*m~eVPhVOfPGYDO`5t zwY>M}4`aG|f`MqE>npjLxRJb+;85t^3Sn=wnyEIR&%rya5jLH+5pUe@_3StMb?p4= z9T?pk+?Kv;JX)I%zyvS7s+jk*O+p-fONso#fOS2)j*}?0YS7% z16w)*$VhNZ7&D4rUwR4Cr%l0U873Z9KxkFVgV}`Ch0`Kc@uko1 zf6{oax$2i}u-@p<7hEq2)vixz_Z6LFieXYxKQW?|sAy|cu{3??<$mUcm-*yLCv)xf zH{vl2HVaPBEm6d^vZJw1;_FqJ6Sx!4kat-xutp)5#PrdiVg3Szh&e`L3f3n0 z81q}3#)wN|D&;UZ(2U()y*)qr!TD@6V;ZRhp1_uHR`sbykEycbLZoOx(&)A<@pN}l z4ih;%BV2O%uleImx1h!b*+kil8-A-CFB#Ka6>vU^tHmQ4#liU?-Zkk-4O?unDM!Bb zVE*f4ALf%E{|^q_|Fvv3Z7SWVA&nqqV+3oAV8xb$gH#^r>Hr%)H@A=y-~QKS>~b@QZynUhkTHC9B~+* z|IDWuKRU%0V<+4A8Liq&%ZLg>_k)zgzNu9}TxguvFeA1vhr{0eK5o4Ac9aCC(vfHm zQVLsLCzl7mI3ti}2+FjLrf}pDZ)5iCec5ux=5#m00xbaBwo#0}bcy!3Jg+^!;>6JO zhM!z=86WxhNjOPSN%2~d#op)PnsylLxGeXuvpz8TAreK>-XZGfYNX8CdMmb@wKbC` zPo}Gpu+I4LY_P!;CQO*Xm{Glq9XposV@G3*AxX+w2pR`UY2n}uIG2@`^>Uo3NrF#f zeN9H0k&6+bS9tN|0WQDlD$Y6Y``mT!{Sg^bGOsb!^!TF1O(ahIvypEOBMuq~yngRJ zIQz`g7~PwYm@s3+7SrR3v7yFhLMjP4CZf|3^_cB@bs1&CDKk*Nj+Nx@|SO$}{x`yF&J z_ulspoFsS>JW5gkbZCx?&(SKdT@e6S%q%W8SzV-VMA1Aer_2#ke;cdlqet~JZuA%? ztuuiQrmWA#(>7$wEw*6B7O!IZCYv&O-HCCo2iU-Pglp!*VJJu?8!Am~eNAs^gTP(G zb2L5lXP&FBzMAj-;78na>+KBYkgLU*gxm**ph7=ccRCiEpv@+u8p5otX7Ii9&SAYt zV`*40QG6Y&%t9Jm*B%CowOH7xRg3HVabfGGs(52q(62oF_!GS4&?A`t^1@)x?*&H% zmSy^B>h9tpsPLxMQGB?eSt9_rssV6_WqoIRrNVYcjg17b$KYwC!kC^e&OYOG_TGIL zdQ&jIaAWp0XOgy;RcQgnqMeHtIZgP(2O~K0D1!dT$3Dr=FTD~;yUD#N1;QE^TVW3r zSjM;zeNHN#QQh5q_ERVFw!;r$bWix0f|8WHS&(m_Rd+#IL6m9^ZtjAP&6`l%@o{+3 z`;I=AtAGCoT%5sCY%8mkrZEVsZIsBYU;u21S4}DPu{KCrys!KW>pY(lq(7Rwz{f_Q z6qLlkm{FsdK7A8*+u<~&9CeqHx^ln06QKN=f(uf`*wBC5--Mt1;!-~PiIdQz8|RZ?lo^jq;9m?s}!oi|9InVPjKD6!@Z77XP zi|DzO!9WQP#VifyT!^Mb>FVlYqYbCB?KWGo*B-m`+P(H<{mGLs8hUdR8wATX>M;Y2 zXi7iDf&!5!F&y+Bk6~cYbM^19=NqS;!C&vb7d1Ba=mxv4BHdOM5@Qh`JR6OA665*o zr%vFg_Z&%rCryL|jT8Gqrc<(L;6AtD|8JgMITk{@A1W?596Qs?Zf(Qi2vuF_N^IrpBWo7FshG zIHbW0o_TIQxBmGyF2CwZZvOLaJpRN}Ve+FCHuv!}n_$5odU9(FT}`U8yc}RV2x8qTHljh^aqc0e+PgeFPxqNgEcQH8J8C)!WN1JMF}~j(jV7@3{x# zM)iO?QY$3JV3Qc1#=+(V=mx4T&l%{`c~U6nK9jLy8*~{HU4tWMQ)sM=sjTel#THs$rzMJsW(ts$MUa6EuDC$oc1<#arLJAE|*9zzyOE5t8#%)=S1xwB35bJl==r zKFYPfzn&|9dkweG`6~aa4|%OsIQD}^b!Vh52gVaT&|_RqMG0p7+U%=x>Ljc`@WXX ze)=Rfn7j^&C0e0*s+lE~X+{Y^1tJ%qC;==pEPc7M{my&w(!xP<4N@OZ)fa4Q8Hdv< zgDD(;L^RSo;o7(KLyts&s~-Th%~HD#fQ4nf6-BcF4t~@AeCumprMJhBMrDMF>!z3! zq|R6W=yc(9xD?Kp-fW66IG~(y_ILR5>1UE*&?Le6*yrlPsqQ0x1-`PUDs+GF5 zAt!w7KltSF$I!4~UC@|0mjzzN*kJKQ)UOFV@Ze)ya_Ob~>Q}$wfd~J=Kr=^df>#5D zQ%sB;3qn1lLpo$Eq0|jfrEWTN00vqZ$JD{db@3;J3_;CFb>Il-M+49uPP3|Id48h-qfpK;gU?g_dtYmvkT%O)yE=3qr0 zA|*>beTlPaYr~*a>-}0o0dU#V-?o7`!_E$&Bh}zrtfPiHnAjmk1E%Hwz4#X3tN=d@ zUso4|*WHO>kKJC)za4cH`@VKBx*Iks=t8$m)#^w&W4PqY#L&zG6>{p=zs(tEe}|?T za@uBLc#Uf^rn{PvCYEis+JZ0q&u5sm%}lxz6Gi#HOq=V!8#IyO5TubX=-}nSCcEvq zFVD|k7zWI17{Q>p7TSo-@185nHh)CQ-bxL&lJNu5Xgemg7x4huH2BmzRH_7{0WBMqtCqb|x?_CxUdlg_w3 zw99VV`_v<>#r|tC2`Yl~j)xzf$EBBD&SjTfj+m4!wwOth1`92lRGe-3dOi%cH}QU3 z5q8;aXC|&Qnd`2IzR?|Ax|XZYn6m*G9k+Iki?Nk|jHYvJw^ zxxm8o!6aZUxe9|G{^R4HWX_#;m@uY?uYLIxK7G;&yyso-;FgluVpda&-#(O6O*cV*8M zhEu-sH9qpOPjcS_^Dte#c$1K;kaTsI>6Q_}_y8Uzf>N+el5&3*4O)fT#uGTc8*ca` z_uls)HoWe5C7*0{Nk5aFz<<{ahhV19x@F?wPI?n)|Ep^0qnsNnW`bb z6r&WcuWjI0!Cw8qSoX!LEnh8tvEs{;yEkF3vWmu{l$PMN=qg$C>?_VzKFMR0-Y4EWfMuEXB=ayS;bvUR>DEdFOwh)6O`9=U!S!qjxmMcG1i| zVhd+w${tbm9_!n0a*TbXmAld?!?2PBk+cs621V-QH8{c)#+le8xHX%K__wjzsIkz} zYCd3wPeBXmL%lFF%=K9j72qKOU48Ut`_rpgg2PdU5cR|P7aDDSElZfTR!(wr@$f=W z7mO|Pte+$mwx01SK6Lc^IP@(C(y*4KsFT_-d`UEnLY2gllaLAAe)j_$a`-#w&mnh} z@vNhgqbMGXVxkF!RgZYb=zQR3tT?tf?=A3xzl{(Q$@ zqg{R@6po6XWIWytGJX0~Zod91@}{H9+BoD126E-JGtc4cr=Nw|2DuB~TP`*fg#K$` zaXP?P90$0npTkE7g*80dSMcfRc~PW#%I=r#l^t2i1loNT6wgxZQ5bw}O+ zvQZQqr*vf=1_v^3xan5@`%|Cc-iIEBBt?=iJ69zwHG*}HsTydW;PM)$I7~ECwP{6? zy5!`ld8#F-mBZ6b3jh=q&yw5g$ibKv04kd0J9Pl8$c4VBR=6cAe)-M`wVGi$rjT;B z%BAYprJP4xi*O4;b`niRi(yb=l&slDPimR@s!ciZzdpw6_IV8r6Xp?zZX$=~0W_WI zA~W#d+-G?0es5xMP=nw+dIpLOX0=t1a-0NcEmjokqv@{8>26rIowYT4zh-x~++qe3 zCQRUk7hdF7SN(>|uDpsYSDb{kW^IbgtHVHXWP=NN&F(vJ=_MD@AjrUi_n&x(AynttXz~cQ;zhC6# zg@dHMqiK4@TN~zx_`=K}$7lp;uTh8`s%*tzp5Qv&d9zj>2zlfz(&z^l{OE!^l9X2G zrpC!R7v~k%?opj|?^GNxrL4y<6#%aID)3Tt7!<{tTG3{4@2L)e?ci6x9!2%LW$1X+ zjD#Yrl=2my57MxPH@$H-Uq0m&)}1(pt^|zt(V|OGn=;_xob!Lo=f3z=oZ4WAR%u5yTb2o5MsAABLF@dHd8QS4t~@A zeEXZH(lC%F1xHv_dQ1DIb`oAYfNJE*azZ8ViYtH1>1UqB{r5dUa}aE!!GHsA18MHP zkJefat6|=(C~P0`83#tq|rURCx);`B08 zrtI~IF>JH~Fzt{OwA0wib{%B9f8VkLpbi62izWBkz;ZZ?{zU_+mLnuvrves0YSau# zr=81?#KsoGB>q|yq1-{OiBGn{_8m;NG#aCMFD{CO3wyLkDo4Z8zP^60 zxZ(=#_{$xvGkFS0S1x?{yRT6=|Q zhH+kuQ&a_q2`Oq~aNfmf13s>HgDYfQ22f9G4b8y;UNwC)cHUuDG||w=jfZg$VndAg z+Sk68=bn3pJN_~Un}(j~pm;SM4yGl~>N-h~R(S-tJi^80O<{`@)=THHecCo#qDU3R z2T&@y0TKc6O?KRFYkvB}?=iY7p^-{y2PNgkFFHGHx#0sTY-+pC;XHT~{P6~xaQ+W| z$QMre5-%<2qtV?H1)?cNlHjOe3^_ST5@th87&C?wK6X6sIP?%kcUuyIHfvDf@?T%W z(Z?MRwufAU=C3Z-sIIGD&ck8pBk|>l+?SQZ?eu$Q8Q+FqNr1el^&50@RYAZ;V^ag< zg989`cXyGdDGU1hk;F!8ExVY4ju~nlM_ap-ueD;TlnntkF_AVY)=&~;Ts|It8q7el z{skQT=KZI`E^Ehr*wBoX=fDaSd+k2t*Y#m*=B;aRs@gT7*_jHxd zWnIBih-J~DuISGYxZ)(t+G~tLY!`!>VZoq7O?S{{N*YZ}?GmK;VIQb(T_e?`x7P1z zA(EEDv|zZVXOAxr5<&fXu1%5_Rt`KmLv`4YX#TmV=u2P87~$bZbIZ{;(=LwwY2g{ z;Dm!zC;CA;Y2mfY(W=pO!&)J=LVtgTcSJ|+x|wgS{msDW-Y$On;~z7A+-Oo8-$yYC z&q`_kW{?gAGtZi4pJmpD2Ii8;E#}!uRNQ+1Y zwSsEi!Ucym)2df~()b=O{=o$_Y>@f{P}SQyTlLI40D07*na zR9sKen6{HH50AIp9AM4QPo)ATYUZHfq#Y{QRo_Gy;M-@M##{D(BRw|E+ztGOmSzYo zsdP+$r3Db9Mh<0A6BZ0QUVq>rJn)Z4Ns}I2^hEQ{1q-4=f+9JM6h`-0KKrRpam1kq z(`5~f*k&T4;Uta~SDQ4*B=qj?vF}^B>z)S!pNLuNl(kc}c2zE(MX3N*1n0P@=UNfc zsg;yO4XKtFqVqyfUDx}CNJ*FK`G;Kp_&nk}3>U&<;|?n%HpLrDR7oTwCFk}(|B3b2 z3FA?NLv|ipC0!h!U?^t85H+w(2UlNv1Mhm@F+nKp9VTgz$D&Qa)fWQuLQb_PS~o1RHgDfm}|EFmX&T7k>9V-ubpeXqf2J7hl)=;OOi;m^6Uj zpi_Q+=~evouD{Xf?pkKPZgq0D?XSH&C-?HwE^7*Ry(nrzYps-OWVz?yv~c1v)kT7N zYidJtr9p!RR5iAKhN`x@F#9Hvj8WYUe)Rndm^^802?SaUT8cnfSi=Q@eW+BY2D%d1 zZ{NK*>OJqmkOw=hVqQ(#^(g&1b&Y_V#dCm5v#3^93R2p_+oZ+zNF&*T!p?&`R2FJ;{zZ32;JSILi(a+g=}J@LF2U~ z#gUa+xL6U-x3qGs)qH$+{PIy|RlY}CC>`3rmi|81zbrukVKj_rMT5JG;K{Q=@Hygg z8b*<*V|zK+57N^Ykti>d+k#8#uu&E5WEe=7$oW(>jtTXeOxN=xsy+1=k2#4 zEkM?X%5)j=wusD4`L(9-fvde0uI}hurod7kwqr_*D^ZWS468Z2?7SlvUw8qddrWBK ziPD3@D6^PlA?o88$)fseTFF@vyoP~5x7~d&Z#?J_)Hd*%V3O{#UCnFohf|+p#j)`Q zQ~2(8&SKk{o6(g78+9~}zf@(Q*+i^CZ38F5V^2KCzx><3bL(w$=ovj4^=S#iOFLPs zNUMfBu&S9NYtLg@*U_2~1=im~0eYnaxwZ<~%gC?8RJ|oh(u}rVv&05rc48?`^mD5QKNgv_Vf!Jc<|voH~(e&vpme0Mq}XOOYm#CX1Zn+LnPT~2kn$jENXMDTt24K|Fu3} zm;#oHXUe*h_{C2zVzX(Ju^O%HrKUMD0n~~?({+obVquoycyx2=%iy3x-^T5C-2+J@ z4l9y4SJuQK=#CV}PTOtG8Q=ICn`}6lZc`d4mE9kRkb8$pNHcU70$q#jUe7^-9CJAd`0_> zUXhz5lDKVSslErCrv2FYEB$Cibhfy ze$>}dF~c_iUTJW~(2=i89+^p$c6C|9)-yNf>tFd2$A0ucL*C@`C!fTj2fZE?Qd>Fxij?l9l;Xeo zbH-U20r)DtlM3AVw+H#pkDtI`4jx$0Si zs~1Gp07=VIuhybFL)zNHiZ;38OItU0t&MJp_7p{%W5r_|&6zN!mtXw!A|{UQrMqFU zvGOY7!XTwMInan;Ot7>lwu)%$J1i#5N$vet0YmYJw64<#vIf{}^XUu@4sz?C|BMk6 zyFx7LKx}ZKc3B?V{&cZ;v%E#3CC)cH85J(;=2=mTw=@S(hqxxjJNAFWzP$I{@4!1q zjg|*ogpt~*=Mk+hxS|V0%YPdi2E+6<;e`J_nHLxIL!5|)3VEL6d|qk|*BdvM@1FTB zHkrCE#wm%3ZAd-`=gIRNL>S1GXXY>9;3MA2&o8}_!RY)_xN+3O{w0aPTEwtRagUZ( zS2z;gt=Lq4N$%^Cj*F;2XZ?5bJm<)_zm4_Q8AoroB^APfOe_Za*bopW9TxJkFlu3i zom}7@X#%Mz|9RX8dBeW1MST|Di+3PO-po+v%OUpdYjj28$I?89SsGquWw9t=S}I#Z zIF7_JW%5MM_}Z!TG@xOlsc4&lf!4W|7@~`qPOMEB9^~M{i+;i_x89CT8@SjFf+&qN zp&>#lo{cB3$3+)>mzkSxh;^mUGBy!h1U_RD2AiJ0KQxy&A9f`7-uDPzy2yQffVT^J zLm%f8yL_cZQj6!5I`fDtV$;5|9);t!X%Qv2S9|N%PNKzH>(4KG4?47Eh`INkEX&wt ztC^&>%3QS=jjD>VX^6liPJkdbMCF`nHgl1A<8_Dq6i8Y1QkI}89~Ka ziL=HiATufwP(fsD5E^={d(PhL`(qF1oLjes?yd$2+~>JeRa3*g=j^@qTJL(-`}zT! zE}qXvKlmQ@+I0sC0)T9di5a2?Q&4$jpoSk`eg)5d?rHq`mRp&qgSu~;Ua@gxCCZ~0qF6WAGw3l$P%vDcY!zzwx0CdZm7Hpbe@UCq@jaC71~ z@D4W^@aDgHJqPZ$JK{p{lc?sLTlTHsr-ZIZ+r-RB(D@DvBW8cVEB@bWShjLCz9M_vUDE>HoOKURIiy2+qN0)qJLcoK0`@ymMTBBjXwwIM{M+zc8*?A z(kl&5I_yx+Is3)9dLozQHrj52$$Ezmb@$(hG{)iU8dZmH(E4<5I*KU1=Gdc;^zT<5idf?t6 zbBmlS6NkNtf(UCIT=1n}OmzRMib_pX1945DjM?6BM!hx};BW~w9X|MQX?#2q9_>xO zuGsqU$fJw4nYQfny&wLVU)}UO20k=oj4ARuHm!-L0Kc@y7iHaO$l^O$p;(hkS!*ro z!U*eFA)Ni<7qjo)d*P~yfLS`%$(GmG5N$5j@jBaL&J|Xuv2$y%QRLD@LZ)N$O_%VW zA9_EVZ9G5f`bAB^c7shDanmqSY+^Snsa^y|d(cd^sDZ10@oUa`<)1UC6%`YV!U?5n zX`yF?*ZtC9-?rw)unY*Z_)$v{TLrNpxE8yHFTbp|C zx$(vudHV6kW38dE;OaW4Q`0aB#Mu0L+E9^*Hf#eQOIk@|Ysn2zhj8}BKD2?Hg2Q2; zi+PM#`r|z|-*PLy{I#z}g=vGtdFa3ef9M-0Q7&`Mk2 zsm~1h=Dwuqjzp^L>ISCF&kHPH4S)K|SF>W(03W5b5;jqYL0rwVpLINEo$h*8f6aL|!-^rTj<|aj%$;-a87Mf;EU01|@U;2hNmeR%3b#fzds)|ITTU-z& zQI2@h;q1BlZdCOIVXD+;&A{2L;SK$mlMHZu(oiwBfgFs%<4~>Gbfd-m)7$=zh5bH# zBT*MnG1dl|@T8)gj^XI&+m=d4!bwwmk49_CSHAjn{O~82;>A*n!DI1hme>#2caL59 z$OqoXqA{Tm!PhSOZq!&)fDxR+7r*gsUi-Q?;EWAfqyy)C;1D8lJ;}%{Z`PS{qvks6 z>%2Z?#G{Wt+POBOF`2DM{L-Z>XC!71zx8&ixVvlXOny}|_-euK|m_+_vK z-nM86UNKs+`I5za`wv|EBQ~@=XD}R0+r<{I9kT=H$o|Tu_-I6#yXIfqt zvd)>{Tu6;2@70u+10%5AO8jC z1@9$t8I!@2@KCSBjxFXxu{n}hWHLB%zM?0B)C0bI(O1}ax1FM4v(H8=$u2NE(a zYv<_qdbnzUc%=|WU9DmM_!xWd@pzv6q$jZV-g~ptV|QS)O*Uozf^h^B6riR17rckk zl2xQ-av!Ag_k(L)lzt0Q6O|w>cgNCudB(F(;f{OmrEpMvLOOZ zN+>A85hR9>eEbu<^X+eDyp@ru`fz&r==G5p>ykb;-mo4)4ZP{i|G>4^-GJ#YK-Ex3 zK~wbu3M&5g^>5(GM;warNl_*QKvxH%bFIQ^r@Z*(=d$#k_-k<6oghvVi1N zfp1ihQ`NQ3{?f!u8%vpPyzbF}w_zU}KD+i)1j5i%sHzHUN{k71sYSnF^o=h?kJ=P?G<5m>05`Fmqn~^@=Y8};%qx3X^@w*^Q7Z4i7B~vj3U7PIdwJ)-zaO5&We>Qx%qdua^JECKrEH_C=m!V+dh>qP;2A-qN88} z+#0_9l`pdYKD!4MXQ$ZMZFZ`tu?)oUqf4&f)EB&jfisBd#gg4mQO3^B>tJ{sXsT?m zF_;;?Y%RI*b;vMB{q08|55JOFHDpw1((Q|*Y4M;Zs)SEEhb8Lr`7U8!B&eQuXfrMP zjCVs&iiw!KiI9{H6qnQl2?deq2&?+2U>y!`nXLOc%glN%#Be*NNDsv(P-&<3&B-F_ zv05X(;yKTHI`8_Yw=>?e6r`-mgAc$jk?&Lkl?KtrPoDo}{_?N>n#x;hERLuI=@qm_ z>s1`M&u(0F;pgZVLQx3f!;J~!a)LL8JMUZ06OMce_bywF6!W4%lHhQ;*|9DGa5gyL zbQs`lg;UoRXMFVWaKQwd{o~siR#c--ejz3e3`Av^aPOP-XGHMvP|Ryu{Nx6knk?p^ z>N49xeapW`Ca-?Z3NM2_HRzxXwlKd_R2f7idT zc>Y-MBeg-x&$-B4k`MrIx#J$*@^^3J>lb|+B?Lb)1qPw69fDGdG8ohxxYvI4jKQW; zQZ(9UhMPR_%FADQF84jK9B)d*dqiSJCS;4wSz&%d3c%|c{PJlXF%)6yrpWMOnXXCl z=;Pt`@EHp6Z4t?|r&32YD-k7m`X~?_D=#!;Wa>lk5==+FUZSEi*@7YBcHT!#O^MEL z;s1p;ND&*89&)8JTH1f|8XUID+^~3dF_XgZgR@C|ywK6jzRf&07Lh(*x#-*cn!VR0qKIuejWKTwIf8V@|HQ_C77PKrhW~h`CK}JS za@o+;X)b{uZ16T+ZWCqd9WDo4qh@60qOL}(0pnwRw%vLw=Fgwc$~7yw`|i70zG4Nn zSIXW3Jc6scA`huqAUI0wmR(l7|v<^Fa(FbhYD4jiP zu{me-t!y;=0!!odlV7h_W}e0H8}!ibjKXc$O&}xxO=A6;Ov)oms3|o~L9H52BU2z! zP%GSj|4Qz<=N^8)Y&jFPLrfo2_NmpNSZq<^d>}H{ZQ(@`3KIa?+its+6{}Wo=;2QY z3N9w_m%j7EpYW2Gy^Mhq#FRjR7)!*9b14wIVe1FNPA3gMn}cc8&rTO;=R!sL(Z?Us zqb;;C)weVRW|crOGir0GJL(V|{@kSYq6RM=(IBvDY?MLhl+6UHOBPy49*x@K(4;(a zCKCUHlmTw|4Pd57-xGMMIU`Ku$bjpG&Q@@m4*@gzfMnSpLvBuZ0q-=5n<~ai;2w!- zq^Sf-@zjGgl!dTp!F&!s>@c?2Vhb+6{BnNwi(lZypi+R8SLdrCW-qp3rqBtF>PhI7UD;fTMb2$fnHG^& zlY{Kt4Zibw!vBnPoe^00bd}16@(|wOC2;e5%}DGbj2wE@#0%#&j-Xi(Pff`FvZh|g zMlGHm1_BNnPAD-U2pZdJ?W1^AtSy4MvwEy3-UqIf8@T*8%CZclff3Z(obh?@5YhO0 zWvv5JUVNRP=pXb<>``vu|J?wXCaW~kQdC|1z9F`S25mPn-2hnYuu}ODEoF#!pQf-x zO-CqoR42Gy1kIjN`OV!?wxuOhImVHhNwB z4(93x58v0h#33JjjLy0r9q|96ruYHy_l``~H!FXNn z+iZXS;od|Ub*)=bohvGj_P%7u#%+}euhaIaCtBI0jm2pPZms=$+4+gLCKOITQdl)FRFsEfl91Yj6{5=#_%j1K#)Uf8~TfK8DgX z0qL%#_wudp{eTbu=g0WfO~0iS!@xU?6gd+VOx7Sv8RlG+h--7|&&3GNRDbKi4k$AP z^G8A;XsZ)HQlWr#mBDM&j2jWvc4lS-jQ1#k!qzje+g`hJ=)s4u(=I!+&11G^^~8XG zf8PhW__E6>dVL&<_mW|=6d*L)NzCE@8F+Kl6;>QZ9iKb@6C85D zz7+UyGH4JUFjiPo!$G#S|EHA1pY@H*nxw-HPYpI; zqL!ZKG= z{eudAQ(e31#?@vjQ8!eEIooXakrepWg&h8H%N$24JfrJ$|1+e-FoYC6fn7x@%JIh@ z&3}IA{cO3};-I++Sq;7{1gD;Z4?dVHue^fa-Fj;fPryX#pbf7t`(J2d?zEo!-`XXQ zdDD8?T8gLTSP-$Zz!g%!hz)0quUI&L9v}X|dsr~97p3lle}J{AW$&{qoE6w-pS@VK zW(}8I`cwM7UeGb1F*l!!3-Dl-6H{H=sbs;YyARVzi4Xt#R7C6P{-*o^xOF`g_e$2n zdu4qNl{qThSs$hG=4o=mvQGD3l0S&){BEO)XpojW^uYc3$Oqrc{C>dPN*ngRP0nye z=vl#tVYl7);QaG1pms`GlsFG!ZGgJ6TjJaWKr@?CXy%8XvBdJ;bLuJ2X7kM!1l1Kh zh7kM`nb=y35v3p)lb52r@=woU$H#7quLl(7!PgNR#)j4g6Pu|_Ue^sb)dpNwO!1Ly zcs>j#v^5>qC8#_c-~V*ZZ0q9I7QA^p&N&v&8{?nf@pk5o1z+#N0zMe7<2CGe34w8} zfXDB=GtW5gkLekiw6-_I;cVI*wRHi!(=dzChJ@J;eyR>5%Dk~YXP)r_f_z!BX9;X_ zZQJarLwybE=v!frXK9Ql(K}2nwz2LTs=HQ%Gq=k#gzNQ484DCAOtJQ^Pc-09=q)pSdu`DM=a4t zCuE=M9YP3@<5oEH^wTJdkPO+L#cAF2Lh^landH4b_rc8F3Faq7YAYrvRn!BHIr^#W zw8Pev7D^*CkG-hk)nP0gbIdUazEjp^cen@XWPC8QfOYXneI%8w4@OzMw%PPU>213q zDxJ0Yxm@<9{pfte_AEyo`J{mJr)t?~AVnJ!rn7wA_!Ik(1s<^PJ{)wwe$@2YzbEw|i~Ew_9OF7g5~ zvR3%aDE{5LmaY%xrhBw}eQ+|NnL6RKxyBFI189BA$~4-Rs+UHA$umCQ=kb5EbGS>Q zcvrO|g$|XskhDd{N_go@&Y&~_M{Mk|8;yWb5XL%VfE!{F1ul@M9!#*!){o%{haQaa z1hBKC0_#VRklQx}%F?pYMjKJrb?5t>b90LAtwCkJ)c^n>07*naRPPBs7a71KtqJjm z6bR-rTYRKpsCjBb!Wf*YvT*(a7A{=aa+ph*oXJ7Z+F+FALx(fya{^B|_+Yl%W^0Uh z6wy92WsbAqr_T1CtbYJh109VqgNThT0D@@E)1G=Hi{_<{3mpZOC^Im1PU)epJVb;&97co!S+;VBElDbIN}0ZSAxc8oWShZqhaD;OnU%Rzxf=tKL8a0XWP?j|R z*GsC|m-vs=jPK!gua$ibLDobLyIgi1%QklN}*Z+Uc>| zapV&Z$JK+797-})H(7aRsOr9Ck5k@F&4Dg=2 z?`5JIpkAU2=Io4x)&=_+H50la1p*!^S>A>V?B>BKX8xc9(sUm;{C(|KY%N_uDY)pH z-(kW<&Cci>GK}>@)H^`%>T%v-38nB!Cq4`D9$bU}PbVU>wrgz8uafv6YHCDSIDY}h z9&>akf7>ZAHoBt?o$~6Cwg)K&5B%?qzoDvwRi#GKp{mQCrt1@!RD4GwEAbhd|KAv{ zVO6NH}5!Rqc z>%E&1_jAO06F(OVe&ls+u2UD%Xun1uRnFV%C6$ts-ax(<;cMxLhT0$qj6r=E9aEx5 z_VE45WoTn*8P>Jwe1H0U>XWn4pjE zUocT>KT=}UV+}mv@Wa@4yRA{8RF*oXn*UuF9_^e2!1PC*b6JYMt_SRYzg{( zItnE^4Za-Bi*D3z#I!|BgYl5}@Ux%&BB(;Q$dK!kQFKdU+qn$wQhqigf^86Y6zSTS zDBvJLU^T%7$&Rp7PGrATZ5c+JN!K}Lz|&!%Sq+_*;+xKVyS+;bDK79x&~9@NlEKtW zCGK=fZB8@7qzpb{Id`BLmAOCnBd|a9-)($!n*sjG>oBdIG#S-gpC)jn^*X#RvsyZd zSRD5VqZ$PvKv`3wzI#2|&52pb(5p;+ph|%;1rMxR#b-bN1)N0S%XH($VzT6BzW}tgs1q z5@pA|({MVgggPK*IKi_aDrmHRL70Uu;$jXbdCI8T8Kro^m?CFg>2*nz`#13iZ!B(_+&%uhcmF`X2B#__0k;2qX<51F|F5Z3({WMd-J%1AsL zEm_D@k9rb#ur>%C_!e(3q|A~R1u=L?{5?)X5W4&K%enr>8*vJ@Eb_=$hj~7Spe{48 z{WC(_{<*m6LtqD2`%XHp(LpO*84W=(W>Hy-&b3N^7suk_Nv^&jj^^yIm4&snBHJar z(P=U;r882fMuHJkOX_^85xZ1kdp9)+wG+VBVVTV|iPPnd`jPi_$}DwGVb{^knk*BS z{5M9tcBhl48k}>D2xx1pEK|Rt8=;NTaO&zXLyu+`wv>#$?Y7;~?V_l}OF@U-B%h9+Az4!Pa`HYBA*aGL^>T7?_@9$lP zH6?ZJbI=XGAUdZfcDf{q$0$+CD|wqX9wh0#N83zeGmeq)tD-1k7F8f(u~J~A$iG*7 zqzqIwR-ji{3Tv=Me3k`bO@1aNoSd23(WOW#y_%;9&xi>Pzwc~p6EhJPvAHyQ)WPnE zQs+6{l}-kho?_^HO{b|fnawZ5rTUCj2RYmGnK=+&o21W)&9#(qHZLa}5)DQ-*XyTi z+29*;@G$;Kqi~KGj4h(Yq|F=MU@1(Al@Rm_6MS`iRpaZ5LW+?2#~W^9MT2_E!cvqr zOkt?Q*bw9-X7yg;Ya1bekSiTEL%4Q$zg|4;A%@}iuL4dbRy)FfeDq`7cmL|3=wc$? z*GgbgCD#1BPu6qv3V7PFPYEm%SBI5sq9x~g!YDJYfSLY44t&PowI)EH@bsr0i#53cu;VOTUm#z!_JPcDP7LwHWxWIgR*Ls*Ok#R>?D z5l>;mLJhJ%4yz77SVKLrf@;l5>WNk1xn?EQ6ZmQ+;s#JpKs|xF8lw)>XY9MMVPMkx z7H`r7DuC!{@ORSbEDiV4jIag(5Nnzay@s+RqD%4p5l554thY4*z7A`EkeQ&3C}u3H zhQBtsXdd;{=JzPEte^a`U7{I@>31yW9^6j!%=@PMx8ose;vp%Z-l5(F#)PXeXkJ5F zP2eUbaI06cdieuXD;~fPR-MzLCB#8HaJcn7XVT}^3Q(1=aRh8sH2{Ul7Y zp`DCKh~jmh*0V!E#Ui%k*4vlz{U7{@TA}i39fwAYNY~q=eXWcMN-;+raRg(%a#Du7 zzK>#ke?ekr7x7nlU$NOni#hDjgRn7}1l&(z`W;4IG@BhMb?pMqBohAJgsf*UfUjTl zO>Ef@#^t{2<~A2>@a$Tr4SgU52NA(ZQxb$zyJ4*x1Qm}pf{4Sp3ZsJa6%)0iU-lU5 z6)ah_m_>^gv1rjE=FOW&QA7_`=UBC31^3;5KPy+R=7AN<8B`T(sx_bm;yt$P;ho3W z63M{263_(24rL?Ff;Mq@yeT(Em470FSAo`b46a>{(iWfh&1Ogj*u)V? zNz(x_Ysg<~z^q2$Z;g*kIQhfxG4c1tf7f8{9bRt81H)YjnSqgll%zW65L@OnQ#0@m zbEYINek7jQG@+VqRA+ig1S5&u8;!=DOL;8S#EP6)ESx{i;>DY?QCvU0eh8cs6BE>x z=l&JTnOL=&Wy>F6Vq$_p9o8WxbY`t8rZ6$^5xc3aCWEr!V+CPe1Nx7W4#*4Xh7ojMnoNlSc7HV)p*{U3O--KiY+BuD_9@ z=uc{hZqNWYlL|)L8$d)j@WB1qbjhMz)~IVs=M#iaTffs<+oaPH%1Nhi$)%Tb*Ii4| zVm$wRVH*9w4Uqx1gP$5fw1qcl$bhVbMP$U|>p>_*ih|8I-H5&R+?_r4*n=In-;SMk z+KDYT-IT?P7t<@tP}&#mYZpEa>YBks%^gec;`Tf49LPxtF5+T{`_$Y zW9jt^Z0a4DaNiZEE4cffdsw#Yes2EVE&S?Nzv7x}f6mXZyPjKbzXSCiYb-`AVvFFs zm~CVkf`CR;4LViv=qEE3+QJ1I0<2)T_|nU`;a9(AugC8kO4tzrv{^(3bycVgMHuf} zjy?J*{QQ4^jf=)fVV$gB5SoQ`;?!>6A2Jn`_uC@h%B2c(_i4r%!bK4vIU=E{4- z6gUrG{@T}YK5~ZAn2n#kFX(J8OxvM81ijc~iMf@=)S=fa5Or}Q*VNS-3Tt`X&f9bB zF;C&iPka))?)EqqFPx8!G`5%wR=x*Hjm2JIF(F4BH~Ar_+&9xYG;T@(+Ck1=R@f! z7V5zSXP*6HUUd3%(YnUfH4_sP*cic$2(~B!tYKu*{YcXR8l}(LtbH}j^gt#7U^?mP zbl`Y`Q_@^$RaM}lv3+52V(_Nm)vtXWS6%aSY%vyspoCJ;&t(-H=irS=8$Mn46m$*F zDb^Ya2|$7vPd%tmU$d~^W8VYz=cz|MnZpl1oSk;qfdvc7u==73ilz}U8lcpg*4mFj zaQ@bt!q%Iz`>s2pPdyT+uyWNJuD|g{uD#~x{P^Naxcthix%a+h7!PI9W8h=fsYQUQ zB6=!D^0bwzLy|ygsyV!73|YiCrE++Ofy*ukZa(%rFhAp6P5oACwX}Z8Wa|Yp}*}_+f{}%uXh?wxL zaL|GKGuCUBxdWRbl_nZy_&hOv3utANYu11|>yT@$B4%=*s ziJQ-RFh%r$b#0t+GBwU;x8sDEZ)RuMz zYa zj9$ChD-IL+Xzh95kl|ZeecdVcmqSbun`V|TefcZA{Op&|AG26BlWv7%pQf;|LchRc zFzojDUD$r>$MBomZjW)pJoEuDl|iV-Yt1e@?ZD1EZWqYUGIhYxR`wA(zp3+RsU3X# zdq3cgyY9yH=H*MAzSqp*`n3{=x5Wltm6+f~;HnJrs~cc(Y`MuMoP5$VdC?0_<1t%q zMq%QDjRV}O*<&DfyCi+uS`cQaqZ}F;#%TT>Y4Mx*|Ghx32jiQ~XUiiF;E9JFz+b%T z6@2+?U*|tRbw0nk>1JFM)2O{*iioAS28#)cMJM+|QZuS`pjl&3=P7Lw-k%FVwFPh? zYcomIRvUjlOcT`7LRJlnOooVlc^q^q|K%yBGz8l<-HHn1E=r}@vIx@!IBX#qT5+x8 zN86;^_3Q+zKozMd4b8-0ZGQWBAOb7>`$g$JSqE2*a22> zHGKLr7vL32V{kf}zcm-m@HrU84AaChyCB-!IVZ0}dZ?TvcpZR>A_VnZU%=w?= z10VV@D^{(hC`&xxya#J?H+>SemYNzSGz&`8o=Rw|dWRsDBxNzhQN-&OFsT%lHFN2j4LO%qs%D)N3k^I^r;1@v?I`^pO4Omkk|9VmX`Ei;bzW`WkD~iDE|b8*@ge>B5B4 z!ON z&*H+*e2U|qdKBVotT==^n65_isp0!z5|LFk!we1$*CW2xqE~jhg>7DAXA_#Z3#GXp zBHH{rJ`(*BzSMlbM24afo#7cW9ijP|41&iFtE)BMOBvZrX-__|Ay@!vr$l|hznP}k1hPkUCUvkmYH8)Iz+U~HPm4+nNXT0{6)d| zU@SjwVc$J=;e+pc7aw@KQ{Os_;!Jns zf=!wr5I=N1Tn>6kEqqjmckUu zbR_uPR(Gz|v+VwreB+{Tg0UD|W~GU#O7Hb)fP^a$l}qsS3K%oO8~*ao`O1Z#;o$vt zWz2#&r8MB3YZj~6&SXV1mS;Kx>%*Cs5D2M4D~LuF9AcK6h@lck12yVvqb6Ki!ej|v zu_nB>o(YCB$368(F8;yyIplzS(Ru=_9;+VlZ6%F{$#0CwCCachWUtp73IdIVf#gn4 zMBK%8vXw-rP4mi`V4J?b{dxM?_;J=x4Rf(!)G~qe4K-xaCNvsH{${`Qr&>o%z&2r8 zBNDf90~Rmn^UintJ@5PXe`c%A7h^Rzo)Kv{&bt7Yq=^h)H`f-6kpgujP4(Qk$wc(1 z^^&@-9tMnaj~Aw4%$PTB))dX$<#bG!YnyZFt`H&OecG0WsB-XivUjjBjqUyFhLG4V88wL3lxy}LTBUI(aiRR)c)Jw{T_GRbvOO-`P4edV98^6-eF?;K0KW=)1->DH&>cm7@;N; z_-NNao2`Do*6yyKw#_U60ayol$V_;mUbvb`YWs~KmtCS)`|W`9LdO!m``<6`I#KH91< z1SY8>4|6StG0I`QHI1^0dI~(HDC4E!X-7YqZ+zuT?6uqDpqijSDa7D>)lr~nUx!g^ zpgp&*8mbHLJy%?HO~4~{M22r3==6c;%SSmi_Yccm5OOJ&V;E zs{t!A3Kp9WE|DNpCbkH{IF9!fCJ@py2ADHf?JS?6vENP!GwJJ;`I<-(isgJ8pX+=+ zMJbxF#dA!zl@4bV6NuS1i8#=3*qV}fvQ}X7ycLmvlhRg`iFlY+jFn2iP|i8$ z#a!^|Px6>8HVznHqyt$3A2Ndk+B6glHrD9P46WGkUQ|+-yaf@&K>=vtu=vg_b-K9yc`Bj*rA7%)L<`6TsBz{(Jx|t_59n8;KOEs}3 zFwK3%Gmk%(Pk-VgY`*dQfX}Fp6oOXquHF1iW4kKjEiIrn6TWFgB1WC?fj48a3|NTn zK6fykcQn#b&^5458FWw;X>cgTqVx>(1x`8f>3sIQPq1X+yePpmnpoVJO*N-=lU+#G zjs$=l$0sZ+ZHY!Fx=+8>MWXhQ>mb2jj7>=U!38dT1ltnvpp2V zj7Y}27tQPO{&)WifBuR;!IIz<7vLA^T31YupPJkxbn5x#^*2ye4YVmy4lgyc^Lvtp zOKf6yd#9bYWs^-d$ra|h_&Dc7eHx1M&~pkNjReiOC5sob)n=RF2M%=_htr$<-M8xf z)KVvcw8n(i{Doio64k(CZ5d9vp}}@9v2zqR<5X#P{@#voXUP7F*wrcpN@`Aj-pRcC zU;lwc^TITUt1fGYw7?*d8u7in4gDPk}$C@it62 zgorR03|fIdVn~F{I>408qQ-GCgVZ_pfJ$y83wCX@S*p=o710nQov#}J!+_Sm6ni% zPKE?Jl&sd=wjlW0<%%gia}pqcE%G6p8XqQN%V9lAlWDhb_jZ=nVJV~oTtb6ntOtMd zm*?`yk9?4Y^Ty~~gJ_t2Z|}E}fkwgC$tNW2-CnQ9%{Sl7@9(>h$}27_5-CnyR*F&Y zadnNcw$*$QjKH3I?m4m%HIt0moMV7hl(w2THqQL9@yzY~u9sCCvg28@D?_ z&~1LCn^P1;5U2E2+{i)Irm90`RXdFbb`T(N8gvh9=crxXIvASTK!?i!nxXzSR+#Mq zZQFPzVtxWUKXz+g_`=hh#bE^R>pa1bu%awRTNd1R|FUoZr*b=56l4iYCC}x$*dUrb z!z+_Mwjma$p33>ob*WvWGdE+FuWR4Hn2MV@8UBffe9ZIV>-kcS7JjyA^#0y0)SrFsHiP1&BVlw1@64_Uf%VtcZIp#K%&T) zrE@OK(7J%l1#M-|(%;|1z=Nr4YVkwC3$*|MAOJ~3K~!<_MI+ZSysRRs!-USJMu>sd zEV8Clz4ft~wRV_bf@4btb&vrI<6VZr+J@hoFN;hHS}9X%h|Ua^>(iCY?aV$N9zL8u z(y|^1Lqb6*NDTOTg1>mpD>?hcFTh6JOd{Pe@Od@gFNqk+s6Awo-D7J#rH$<3*orYi z9Xq08Vz6s9$9&Yx^O0?2O@d$~G{}hSC7xzk0}^S9yEb$q4U~{LpZG+W z0nhn>=}vUWaAQkaS!Z}1gWBN-j$htz1BV^5Z>*NmUNU)fQCdMvdb8T(gEwvPIAEfhJBlGVIj?NF)z+8- zOi|=el2DBVOIAeT15-rU!^VJRIvIep1565jDIVG)yiYAr31wtmISP?@<)>D5?g-Ro zC6ZXt8nc#JVWVu4HlIeU%oZkaMYTexIsN&kaPFU;jWvNZZ}UBpW)!lu3De;}TtQ^4 zR5kJ{615}-p;Z%}`<6Yxnl)?k?;RT(XVIdCjE{w~JPnVCiTG9&_Z=4DFlz8# zhcpViU1E);t@H5HE3e_?Q%_^y3NXKtUwU6Rus9l&H!f&f)rTe`PBKJ;0+8stg0Pnzx);4w|o_9N_@r`c}3?H;bW_F8^&@uggM z-F4h`*WIjGu_EGXP1)J&6>PTorfj?Qw(Pb0?i_XGk?gtqZuG~%wu-Sy*pQ|PsE{Rj zrx+U=Dn$R42?y8y^4C1$*(bBQhC%JIWsy&Au^}6D@!oj#QK`vNj?L%2ANU9tUwmm0 z(xeWUjB7HqHr8fs84a*azF%PipwVs$CAZ>S6%DUKSr&wHqc)tcx%Iya)pq=$o%ixjt3(=8JJFTV6LmM>q;MhnNo zAcT$KSnfBG&e*amA%PA$XR4EA9+E$xy!h8Si zog8xTe)P)F4YPoELG48~oNN=7vT|aA=fB`guDIsscr|$Gqb6h@LR)S}GH0zxw?hd$ ziYVOyy^Y2D8Y4>SD?a+6_j26Phf`>>>Wuh;Z^qIzmcZI6y@v22k(}*0z&W_>j=TBh zcfZG1zWOzO@yi=ov1&EWYhzuginU?5JC`}G?#x8?ijp0++lCXLbv$Rh;Q2gei;XFy zg-WT56}oBhqfGPq;yEz^{P>b9IOVh#Q+aSw;9`f@dym*6V~!%yc#hZt?*r(rzJ{ub zZd{QEsIl3BnZyNK+YVeifF@G5QAa&k!?}OBY2(%`Nd1#G5N~7TtOpahp9V}qK@Wrq2&F}nxU*CK)RUPPKrYI(3Zjp%Ywa!#8 z?`p>TJ$8NEEd+|vtt9RX pkil{o^S-qRt`ea;fE(;{OsiLW*-2}ko{vL|6@qqy5QV2RvJ!v_A@x=th0FB z&fA4K$#?)d2NMo1z2Zt<@v2vG`yF?K6sVLaMYPRK5vM)AHLU*Z^8yGUDikK1)P;#; zl4$-SqLfse^o-+p&p*F46P)u9x+V-kn9RHO*{(AL0UJ48;kJ0--es$J@B2T*r#}4| z?z#7YP&gN3q0yj?=}_t}cC-zpd!y8arb20zUTHY_StszIyBT zXA`D=CxD+_e-lR?`*a*76#ZZ;>Kh*il^DR<87*mIz&BF@1lmK_rjf>^eh70x&B%`C zlr)%@3!oUq_=;+M@bM3Sfa8xn0;7o}Pb0P<5yv75YtUBOvAN7m1ag-f ziE!ypujOC<_20Pkr&qFK%^HmDV|)FWEp=&N?LF8Qu1NG3e2w=t>Kz*|SrnH^cetTl ztkraaVwg~>dFJuQ^8Rk{2CuzRl!n^pbAVoH*=*5#e)#RLv-zTNO4D%d zhK{eq96~^XP4Lo<JYz%Bab+Q^FH<=3U!eR z5=+Z=V8e$7pvFm|HgC@q+0HXc7qEP_^47QiGv}RmJ`;l~8p{{);55zpQSGe#L}DMV zfXvX;h^rd>h9ecLJ*_!ATL5 zlmwTAZmjDF?D|RM_^AURL93CPu~PWbg%_~ruDdW+geFI6H9|9096R;k;}K{q1B4%5 zd^M+>b|#fC*DC-Pvk82ox++2bs5y2>IwepZ{Ez;zi|=&@xFJn#U@KaR^)wj{8CI|SzUQntf@HRpyAp@@KI(9O1 zS}=*OGD>Vsq!G6ry&<~)o`eyS@s)20e1>74$f?8e);GP8(ty>#B@NUJ)0#|?{O;0Z ziV!jSn6V+DgscT_{LSs0{Jb-`<+eL1%RXw#@N-_{Wp_g*fAjiX>=zgD`}0sjdosvG zc7Zy6d+S~N*}1RfJKy>)|Ma%EvUt%Ly&}ARS5+tmTZTT)O~1R7nNC1%a|E*rxCWPziK^x`ul-9t^U061WZ^vKjoDCL;KLlbS0v8Y zCP`$caKYz4$3$IWOu0^hZ;p6?;b!{QO>sz=;T6lk3$^J}jxDAbUq~^&5YwLz#Tc~{ zYHX&wbdL=`H2Y~Lzp)|Byu688;tEw&VVq;<$8N_Nr=6M#=`5ubxO0BerVKAdGbcd3 zM{LPJl?jDgZd=NepYk*=x%?WW>{DZL8lKpK+Uu-th>Y+?&3+dq%uUt`QjVdf&yO#? zlB13}j%$8#1C>|Qq=EO6RTGX?gPLcad@8@b=~mSCK*|`DHG6dAV4E{h895_6iA<^u zjR`f~@wOrE)l^oNqq}s?y{^@dY*0+wxt)anHi6LlAb+pIth$69o?LuwRvfNc%~>yc zAv^A{4S0`@@?0Y*dy})&Nbg8Q<4R$m@U8Fum?s_eRBpfP9?IUlzQ#q~EaQK!*0izzLuD$gyqFXg0Dp2q_#*WkRN z(!kaA8Vpkg}ue<&R&VBW3S^E3?S+UA7;lLXcX+H+%119NInDFqy z4}Y8sKKuE6*sTv&cowt3Qv=vE2q03F*)SU;RF%ute-%(;QQN~wL8S(7%7z?E*zlbp zXeEyeCAx!(@00*W+rf|4g11qe611M+@8A5_%;9*q= zKfC@noN(fExclDaNYSVECXlzIDr%5r_t6ViXVfH2Z$ypaa9P7mBcVF!0xAWnB};$5 zoRgpPe13YxwM;mrQbUcfx>8O#?F_EC_Bx~-LrjmVt~Z4wq%Oi8c zhU?;_GdG*=%~a3qf!z8!TWq#GdXo~0;yTkClJRBV-926I%a9O7cSLjefJyd{!xKXc zk;_TyJl?U{#!GnFS!cy&ly=F?aWqp}n02!5Y{={39t{Wp{+GY@Enf7}mr?ZQ2S`N% zd9RATCdo`)ThHMMEghkl-BIj`?^8`USTvU4lzncxddX)wbQG-3^wpg^3b52EU? z#4N?cVeriF%L9KpC4qUi4~qm+CTY&Mj1W)gWS(%$Q9S7h2OtiNO^U=alsFhB&3Qzd z$7H%ap>X3(w{hxeXL8T72SVwt24gHP&KKut3=`ClhecthJ{F^jaaMO}=suxup;%iY zDtHIWS5=(#@>g=+CqKqxAGCAL40N)09j_=xkR3_Xy?b)C=3G(bL` zEn&N{F^JB zeMj1BIAd<$olu!!f;fruaPR%AIP;}v^R2Icl?4mNSY1_|@zQg+_0GHTQc#rrz+lM9 zLV`vbd9InDsTr>~QDDMYs@ieeo%itC*T0c}dFQ)1{0RrM>*F2=z%94j!nHrYj%%*@ z856FiD0_GnY*FS(v~_(HvkiE|BTns8Hd!iKYf&G@;H-pAC_cdSVO|zAa_qcMY7UX_ zr+(6gYdeiK9<*sqnEbVAG3{M!qpQGJX*uJCrw1`p+ak)wIm57-8$Oe$=B5kLWXP*f z3Eca@DxUYkGg*4y{a||#iO=Gaj$bfGcd0wx0n!i@-1+(Ux;q2zaKWML4R=lI2@^d#lFO4c1};rGmx55+o&ybpX5c&jeKCLj+Sj4Bk9P(iZ9QG>aygf%B_wHofa?|x1?_4)k!d*02s-DZwyF*Q)R*%ZoczwZvNaCGDo!68v`$ty*@(0 zz$?b~BV=M&AB%$-17Jtga5(!D!5V|B2G~M^TfMItFH7c)^(mu=??hcOr~>&F4PF&$ z0tZg4=?WIa4ARgTXGk>0R++rXQnrvsuaaYYJz)2J_TbQi4+x7n27_=T8&wr`&1TA( zY3_0csrbsnOU`};H{EgvrW^|*oYDk;YivjNY+8J2nyzR%QA6h|F!gC=tF`x(*2I}v z!*6fBlOv8k7Qx_6KN5z6xl>sbkqV(L>98gg8b(W)ug{}HX_5AvtDhRfGm!@u6?j~X z9A#OyvZ+b(rjs_)aHd)NiZz4PxdG*6dhv`9fN3?R92U>1C!fSvKOz-?B&S2)kVLh0 zu=xnbkPpF7D*pO+OF8FH&!tvFT|2BPVodPSE4V8}QB_r#KAGy(YI{gv#7J6eKP7sL zF$J{?Rg&{i4IJ0~>Q@|f>>q~}QIvRNan4f|WiWe+isdfSuEe`MO4m6|)J$n)HLCCl zPJsr8$2*TLdwAz?#so5RF-~a(6H9bkS}Gp`xz8%#?YeE<0%0+!|1eyNnv_A97+gZo z*VKq`8ayZlHGj0jc0B*olR505gV=7{Z7~Mwx@PIpd${?xw{Y>Lm+{^2|A5=?T#6Tq zS_w)JfKbHwM~xFBQM|wmc?c&OMoXuzN`4ASnq3`b0na_<*^KoCMmOSlf3u>G$bSZ>HP%oVnDOtcNx^Q%og}BLMI|c5|g1 z4+|!5U>^*c>Z;Qukf97)^NEu6ho;E>OqT2)#(r~F66q(22S=+H+V0+l49|7Czo{D@ zB-;&zbx;V~ZMR*dA$2_?0!|A^-Da&vXEe!5N-I~dX4R@yxzQtHf=gertBeF)^b}?ANjf13OIWh71q75`MFEjT*+D==Z~;YJ z7b;Z>XnYQ${xCopB8v_C(2k_XF&*PL+|4ZdRK@jhK0TUSh?TKq0o_6D`pjI8# z69zp&_ETo2pjI&vt-18?RsAgQl}1$h2fCzPX!K5kXCM3@BZgkezuK$a_ReVa?;X{9ek!?n zLUrt0YE>{*GHYI%4`!wo#lO%^flFLy7QuV zKV8q;qQHBswVBa_Fga7#CzoD;L&VEc-x26K-HyBuN`edB)h0kEpq)GV3cfO~& z2Q0Nom%hnzF$mSAPSp;m{Y_GkRPFc1eX-8;ey|_iSF^g{N;N_}xs|B;rBhGCaBBov zPj4_8aDlTX9VQ9XEsWwt4tYj9klKmcWd#@g3&*y3t?Kni7x&-VCLcA?p#l}>)#$<@ zMJM|3HHk4N)q9&1@34ln?P*v{TWdn!w*HHp@VVD4CV^W#H)`ZbJQ%d5>!0(I_z}1c4azM?uaIg52iRAu>Zb*uQ;B` z%3IIU_>xloz+LzL8Nc}DuVAt!a_`bqv6h|DmIbo9r{(H?Upq!ST6V>n{wOz+xL_T8 z;R0>NsU18C*gKFbz|LvBqW55*gM-0>6_vjkDIobU@)ZL(XumgN&g_}6rvAIi_d{v3 ziL@Cf9xX4)z2y4e-U#RF?k=No65D@1i^n=^PsiS`-xDroK(hoRh|2fGJQ!3XLbYux zLU(`BC70ryW09fca&ZMv6gnSvIr57>FI$D0CEg$UYQZV>Q#LFB2W6x&1p@<23E!8 zqrOgYi9gwBWa#gmfY!)}8mxLXUu9#GwieWYH5u44tj)koSZhi@Hr!wa4m)6f?6dbC znB12IXXWw+Rv`lA$01cr(Ic_tfWJIBA2;6gM}Vyk3s4VZ>cCySSI17+W;AhA}>M;VGQ3;Z2H9}o2^zQ zSo?QrGyJVc4cE|yIr`5DmTYXm924OjSQt~M^kddWGeHIz$y->md?^N(4`BJ=3gm^W z1cn9Pr1xP|Q56`hd=p$P9$+*d6z|K8MszY`Z98~dg6-m5ptJy%40hl3m6$PO2A*25 z2%KfXBEKZVM%R+ssh_YC{l9shBg--+y#^xj&O7gfi9I1g(!@*(`X5uaXk_}BT2cr_N)WJvRzq=WPeASpu(4=)V7TRQ@|RA< zih)6uD8>zHlN9dt0Z*nCs;_UfTi2+%5W zEL*k=4?XxW&OGaX@TXgEL+%Jp(Ic?rGjp4XW~>lSdiIRTT5jyRW<8un>MD3&I=6N zWG-;}*T0D?uet`{hSHqHG!{!$cf0Eb@ z2&&Bh6EN(73opJ@4M|O=kd25=FheU^6s<(ur*v6gs{8!+Uq4-E2oP9uAS2jp&PMpo zx4wzFo6kfR2B?NAE3TPg-HFq%(b})Z9NMThj)6_r!lG?z&M)d> zI*+xdO;4G*FK?JRi%~kMuWDuCu?B4ZMI$| zQMP)K+BaDNaOJhXhUW|(G2FX+Z`IfOx4>@xkw2=FZwA4#pg~F-G2&s#!H5Gprvc$! z9h9RjQn46;THxW2c6=$?+Q$zkz_SaM;GTOQ0A~%QHc5pg05z7i*8W4>!@pk@ zsw{tB&uutp%0c>y(?Ga zz3^BUc{vY}bV?gzkY!nFThvgPW9(PpdMVYxXqZ7ckO*w~hPB9y2}D!sz^{@vv_ogA zLEoxCDFN<@6Z){}CUf+(ma2jzc<)u}mt;=`q!4leuxRlL+>IcEU6q{Yge;XV4U#8~5Oi&kq6pLT2F@B=q2^=Ztn|h zzWJuGMu#7ax^gCVI*~p?ZHxpTYyy6J zkNWz?Rsjl+YHXRYsV$CfRad8DbKIOz%R6lc?Z>j4S5J3e#a}_Hv>3}QZ>x%io0&@1M16(Dql838$|;4F?>sziO}HQRI20aB0J} z)8t&Ynyt3lQV(5MZm#Ve=Snfa(ThS~>6tqxX9}DlmGKg$W$kblNws~bS{&)0nvqd` zh$>`dUuJPsb;0`TA}=ruWdtbl92?J`iDo0d{0fh7^t(|x!F5-@@4*K#+!}#xHqv{j zBpL+17QaNx!;fo2`g=Y^Q3Kvhu-}PoMnaNK+@<4d!zoQw)UM2eA(%u0^$z&!(@*2X z4}Tbcy6sND^uT64XnFOGQ;8z0NvSW_RK%dGNq@@yq~y9oESNcSLk-?C<*+R&pKABL zO4QdWW*%rb_damHl6)yBC`wG)V|pf4y~3*pcSf~TCVJhgh0#Pz)n<7TKf`_+T8o`gp230?v9OB+&K1Zg7bCdEhjUbmS0GHkN(W~sunmXqbz zXeJFS8~Qn)!&-xNrcDp@mvPMILuKk6ux!~77A#m;QwsfKVQ+uiwXxFo?>sizU`84^ ziSxB*G9EH%T_kO}b`gDjeO(XbmHl3!N{6!gJ$)^>N)m@eZwJ zdu*ML)<-%tS+ZnF2M|4;)~-5{BN0rVI4LYmsmrxS-%%8t1K)WejR)vGU zlvQfCpX=KmrTPRCw&GNU2rb8 zp1TPeESlHh3Rs~^_Dh%7UNOFi=D3YU1Cu6AYFn?}U<&G3gFg8G4GoRdI-B-3aCNof z|G(qK9qTy^LHhc7QUOI39FL9Z!~mrnq#8oX*d|hSScu*^3oyeq}^0f+Gw$K-TPNBd0cB~nRoLfi;?Or}hL=pGaBzu1YSl)hj>8>`qX%g}5# zSK@Tsc6}@P!I4usGBOfmb46v%)~BHV_Ytm&1N!@?1R5Qu$OSOL`M~a^vgJ+25yHVu zOlpTvyR7v^Z-A@X9f|AHwK5*DEB8KNKvb!=mW3BW$yI}y1L!@*A$3&12Pqb{8HbRd z{8_bfI4sDda#wq#mMao}tbGGm*@N+qa?-w(A01VH1=CB(a;fU6;IIL_=BkHY^=JAz z-kK^U;aVk#FsLASh%aCv8g%6gG#L2!M?Zk?e)CN9WP+v*bR6-O+}rLLaEuJ~-dFf} zlr+Efyp%AE@ebmBGA|8n16oB9d;uBasjIocKl>&a2S5%Ew$Z5B+ePt&8Z4JMvcPq$ zzZEN1)V{A;5R9e5=FT43*FIGvYbtph&9e|)f!i*C+^@7uv7u%uoigbe6rOD`O zpM6*pystIiMveBmLJmmVeNC~NA~3`#T6wxY&gEtJy4syqjB5in9Dkn)_UIKmcrrT4 z4Xi=s^hx-;FqpH%GRTNw$)m}@zWeNsZ=ZcSGIB8FI^9kJ1txBKPKxyZbRB9ZP!b|y zE=MJ!CenFAbsEX?36lyu#n_>UCg7Txv zlXOlxhGy-Zm@<{h!AISL(~MyQ99MT#y0FBLq9{@)AX3`RBc6!i1QlOq{W=G*1s?s& z;}{+pspa9;PsdfiycjedPw|nFk&>Cf^$BEIQP#6;opY-1^a5*vo}PxfDJNZ43BuNR zZU6V74|X7?JLgaoKKaU#u}TDn8fdZK*qpcd%PN_opv(B>tIBu(^MPQ#0Rxx5*WCmK&#b4hvR+QOt?C*s&i3+-oB<% zIU;3CsP0Bhz8)v~&)fogug_C8UWH2C{iy5`@>)w(DRkt=J%nQjkJ4(^TfZ`$Dn$2% zMLsl=BRbt{mPHI(=Ctu423WHI#=&M<%{}$hQy3l|t}82of}0 zpzx&j?_jjR>Hy9bVk{h4cvvM0vwcI!VZAsbYzW4N^!3jqHC4 z8=yGBn-1C!$GqhT_*0ZWYG;XxRqW~eaehD6b@_Beys2PIt{R5N| zDt%T0y|IL5vx%NY6Ai1?bs?q6s29ZyWA~te;Z8TO7?bU#)&azpshMe>#Har4qqyp- ztMJ^C0a(u9p$3}~=Zit;ak}W7!_uWoYilSzctV})$vddLF5%vU$&>prsedAtKet%z zh+-2F3G^bs|II-lo&xWe-~d=s+cfb$I7P?o8cKx>Ra~JwK@Y3;0pJ)a>-ZeoRdvNw zj`ZpQ-XTBWg%_3}cY5kFsb9Y_8hFemdi<3dJum9o@%-Wy_}R~YiNbUExA?n#*Nv_k zrd-!$IOR%ut>m76eo@f&3Jep7d}WwX*}x+yRv6Cd1K~m@>lT}Ds?-b?knc=V^Mt>l zmC>qB)xV8?bTm-e2dft*0GY@2H~kTRI^u2cMJo&nBGs9}L9=ZwqhT{-S%#iQ1N{>x zV8)CYm_2htymIHAu-z+Oj!6@mXs{|qG-b2^1$XDN@&XWm@hGcQ{7!K`YZ%kknt+de z_=EVwXHEu@>edb0N-nK##xB=bggh_s+@j|zRbSh)KxL+chpI?|fCyd$-U<48Gi*G2 z4xV~;A=qZ=unpmqwb;{t)&sPRuwV=hw@`!@*jB=!Vhdba_M5V+44mD8fdP03EUPKT zcwhxj7<(p2nuJx`Q#SbCx#!^RM;{)TQKSP6DQPYoaukTvo2qa?3&7{T_@7v^bO1ul z7lnl+M-S4pw53ayqYxk?QtCt`4Rx6UMjXZ%70g>pa5=Ee)>~F83X670SInXPXPVfJ zdi`T!od9fmuz2|(maZ61SWZj}OT^bWG?SnO4Fibi#j2$M<01Oj(_*DnA+b9 zE4qPrFM+Hs2@|XI{tYZ#G=Llq-V==F4m>cfrx#v$VWsz1 zQmPh}{K7ExHP8e8W-caK1`X?_kRT-0Kiy_C!Tl$(c;sEP~^#b!)r+^e3ULyAntZl~w4?lwMopU~h@|b_BSWdK+Ptl3sr_%y7?uipa6q7=Mz;%zep|m zC-yXP?Awk>5e>F7nTvRh7#O^`L|YcHVIZ%@HQ`|4X!3s#Ayl zcG%qb`JHP5Ctl?SXfn*3cU!8WO4qc2NLNJn=-$p04f&-5o? zzXC+0iEJjr7yk29oO}L{uw;1)Ef093|K>?g!x0B8TjB7BKi!E#4tq1sIqyQWJVC+( z(yh%JYkS%5=lAr}Ph;7@APT{_7~$0=-OLR3+3R%}X$_@NC#pm+%0D@N(@wlr$rp9z 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 FV1hFY1z!LF literal 0 HcmV?d00001 diff --git a/src/www/icons/logo_16.png b/src/www/icons/logo_16.png new file mode 100644 index 0000000000000000000000000000000000000000..574a0245a94f8fe7bb30a19d87acad31b9b5bda5 GIT binary patch literal 1335 zcmV-71<3k|P)EX>4Tx04R}tkv&MmKpe$iTcuJe4ptC#$WWc^QbinV6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfc5qU3krMxx6k5c1aNLh~_a1le0DrT}RI?`msG4PD zQb{3~UloF{2w@aMh(nN=sn3aG8lL0p9zMR_#dwzYxj#q0QZO0d6NnQ`H!R`};+aiL z=e$oGVP#1nJ|~_u=z_$LT$f#b<6Lss&od)NHZxBgAr?wqEO#+08!GWMaa2(?%J=77 zRyc2QR;zW^z9)ZSxS*{pbDicGQdqkM+H?_h|#K%Vj@HPNe};s;}^*#ldA$o zjs?`9LUR1zfAG6ovp5xZlfp@$_r}G}A}v1w!)UAUZ*oL&7bTF}7$0*bmYO51P zATmr;BpLBWqa+YCh$C*27}AI&MvW$O37I4siA3g*K&B+b6SBm(%w^0{WfZiwtOt7B z+I6sX{k_;A!h7{yeEAM$P++Eo50Q4pxtSMso}3Db%?B9SQ3M2eA1ql|`=6z=K< z`F0@y4TdV#S{r$F*DI8jE@X7nN0COuj@IpDvKf9p-%sDUU+@NF+@2#z6$ZBeY_d1d z`N1I!y89Ry9Ad=drTuUhMtv0;jlyI$hq<91hkXs{XqbP(iMhp^TASKfQ_ZpNk4UD| z4E{dMr^k9pW^a<2&I3>^i>x#*0idI852h7$ytKQGi~cB5ab=DB@pY>xFI~vxD?Ylv z=p~SvWO}x6=g)jWC7jAKk)5J)fkHYxPArkYABrK^nx9gSf7Zo|JNJ^iIgQ!0oPUEM zVyO&9eKi1AgHdJ+Di4{LqpB*=Sd#9}_h@^|jclr`B{UXd$>J*BIoOUQN&xhnJk7Re zwqnrh=sa>1o7F;7qZt+G{pJkuc${XZ9oh82G6KQtY=5qW8#iL?J8+mqWeW&Klhhb= zY~8X6K@eE8+QP2a-ee*(NpfNmwNPMYW(F}Di<8SuApnvjAxk2PBBQUW;L@L$84HE6 zTAC0A0Y#CKWC=xJ~1igP-~b5VXte>XAcj81y=J?{^a(&oQgs!|S}XvdH@f_E1_{%EdoC zd~*C-B(YrY&coXGH~CpMMsAAV^V>hOc@~(euM^6x4Z?=TZo3 zAH1I@nw@Oi@EGOgWsF|&5sxRSu3UuK+`yM7dKn(^a=~-$PAY|a<6Kdsu-deg4eMRh t=yk*s3A} JQs?7PlXe)cY3{sRP)N#>cdWTF57002ovPDHLkV1jh&agzW5 literal 0 HcmV?d00001 diff --git a/src/www/icons/logo_32.png b/src/www/icons/logo_32.png new file mode 100644 index 0000000000000000000000000000000000000000..c08279e9d9a12b365c35a6f5bb5b3478b3dd4e4f GIT binary patch literal 2702 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 literal 0 HcmV?d00001 diff --git a/tests/test.lua b/tests/test.lua new file mode 100644 index 0000000..ba939aa --- /dev/null +++ b/tests/test.lua @@ -0,0 +1,24 @@ +-- Include NPM dependencies +dofile('tests/generated.lua') + +-- Load test module +require('busted') + +-- Load driver +require('driver') + +local inspect = require 'inspect' + +describe("Driver", function() + it("loads", function() + local start1, result1 = pcall(_G["OnDriverLateInit"]) + local start2, result2 = pcall(_G["OnDriverInit"]) + + if (not start1) then + assert.are.equal(true, start2) + return + end + + assert.are.equal(true, start1) + end) +end)