pax_global_header00006660000000000000000000000064150344750410014515gustar00rootroot0000000000000052 comment=aca6104f0b0fa5dab126ae613874f9a33b709463 Qbus-iot-qbusmqttapi-aca6104/000077500000000000000000000000001503447504100161715ustar00rootroot00000000000000Qbus-iot-qbusmqttapi-aca6104/.github/000077500000000000000000000000001503447504100175315ustar00rootroot00000000000000Qbus-iot-qbusmqttapi-aca6104/.github/workflows/000077500000000000000000000000001503447504100215665ustar00rootroot00000000000000Qbus-iot-qbusmqttapi-aca6104/.github/workflows/publish.yml000066400000000000000000000021351503447504100237600ustar00rootroot00000000000000name: Publish to PyPI on: release: types: - published workflow_dispatch: jobs: pypi-publish: name: Publish package to PyPI runs-on: ubuntu-latest environment: pypi permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Set version in pyproject.toml run: | # Extract version from tag (e.g., "refs/tags/v1.2.3" becomes "1.2.3") version="${GITHUB_REF#refs/tags/v}" echo "Setting version to ${version}" # Update the version in pyproject.toml (assumes a `version = "..."` line) sed -i -E "s/^version = \"[^\"]+\"/version = \"${version}\"/" pyproject.toml - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.x" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.package.txt - name: Build package run: | python -m build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1Qbus-iot-qbusmqttapi-aca6104/.gitignore000066400000000000000000000000541503447504100201600ustar00rootroot00000000000000__pycache__ .mypy_cache/ .venv/ dist/ .env Qbus-iot-qbusmqttapi-aca6104/.mypy.ini000066400000000000000000000014421503447504100177470ustar00rootroot00000000000000# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/mypy.ini [mypy] python_version = 3.12 platform = linux show_error_codes = true follow_imports = normal local_partial_types = true strict_equality = true #strict_bytes = true no_implicit_optional = true warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true enable_error_code = ignore-without-code, redundant-self, truthy-iterable disable_error_code = annotation-unchecked, import-not-found, import-untyped extra_checks = false check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true warn_return_any = true warn_unreachable = true Qbus-iot-qbusmqttapi-aca6104/.pylintrc000066400000000000000000000242641503447504100200460ustar00rootroot00000000000000# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml [MAIN] # Specify the Python version py-version=3.12 # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs=2 load-plugins= pylint.extensions.code_style, pylint.extensions.typing persistent = false fail-on = [ "I", ] [BASIC] class-const-naming-style = "any" ["MESSAGES CONTROL"] # Reasons disabled: # format - handled by ruff # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load # abstract-class-little-used - prevents from setting right foundation # unused-argument - generic callbacks and setup methods create a lot of warnings # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise # too-many-ancestors - it's too strict. # wrong-import-order - isort guards this # possibly-used-before-assignment - too many errors / not necessarily issues # --- # Pylint CodeStyle plugin # consider-using-namedtuple-or-dataclass - too opinionated # consider-using-assignment-expr - decision to use := better left to devs disable = [ "format", "abstract-method", "cyclic-import", "duplicate-code", "inconsistent-return-statements", "locally-disabled", "not-context-manager", "too-few-public-methods", "too-many-ancestors", "too-many-arguments", "too-many-instance-attributes", "too-many-lines", "too-many-locals", "too-many-public-methods", "too-many-boolean-expressions", "too-many-positional-arguments", "wrong-import-order", "consider-using-namedtuple-or-dataclass", "consider-using-assignment-expr", "possibly-used-before-assignment", # Handled by ruff # Ref: "await-outside-async", # PLE1142 "bad-str-strip-call", # PLE1310 "bad-string-format-type", # PLE1307 "bidirectional-unicode", # PLE2502 "continue-in-finally", # PLE0116 "duplicate-bases", # PLE0241 "misplaced-bare-raise", # PLE0704 "format-needs-mapping", # F502 "function-redefined", # F811 # Needed because ruff does not understand type of __all__ generated by a function # "invalid-all-format", # PLE0605 "invalid-all-object", # PLE0604 "invalid-character-backspace", # PLE2510 "invalid-character-esc", # PLE2513 "invalid-character-nul", # PLE2514 "invalid-character-sub", # PLE2512 "invalid-character-zero-width-space", # PLE2515 "logging-too-few-args", # PLE1206 "logging-too-many-args", # PLE1205 "missing-format-string-key", # F524 "mixed-format-string", # F506 "no-method-argument", # N805 "no-self-argument", # N805 "nonexistent-operator", # B002 "nonlocal-without-binding", # PLE0117 "not-in-loop", # F701, F702 "notimplemented-raised", # F901 "return-in-init", # PLE0101 "return-outside-function", # F706 "syntax-error", # E999 "too-few-format-args", # F524 "too-many-format-args", # F522 "too-many-star-expressions", # F622 "truncated-format-string", # F501 "undefined-all-variable", # F822 "undefined-variable", # F821 "used-prior-global-declaration", # PLE0118 "yield-inside-async-function", # PLE1700 "yield-outside-function", # F704 "anomalous-backslash-in-string", # W605 "assert-on-string-literal", # PLW0129 "assert-on-tuple", # F631 "bad-format-string", # W1302, F "bad-format-string-key", # W1300, F "bare-except", # E722 "binary-op-exception", # PLW0711 "cell-var-from-loop", # B023 # "dangerous-default-value", # B006, ruff catches new occurrences, needs more work "duplicate-except", # B014 "duplicate-key", # F601 "duplicate-string-formatting-argument", # F "duplicate-value", # F "eval-used", # S307 "exec-used", # S102 "expression-not-assigned", # B018 "f-string-without-interpolation", # F541 "forgotten-debug-statement", # T100 "format-string-without-interpolation", # F # "global-statement", # PLW0603, ruff catches new occurrences, needs more work "global-variable-not-assigned", # PLW0602 "implicit-str-concat", # ISC001 "import-self", # PLW0406 "inconsistent-quotes", # Q000 "invalid-envvar-default", # PLW1508 "keyword-arg-before-vararg", # B026 "logging-format-interpolation", # G "logging-fstring-interpolation", # G "logging-not-lazy", # G "misplaced-future", # F404 "named-expr-without-context", # PLW0131 "nested-min-max", # PLW3301 "pointless-statement", # B018 "raise-missing-from", # B904 "redefined-builtin", # A001 "try-except-raise", # TRY302 "unused-argument", # ARG001, we don't use it "unused-format-string-argument", #F507 "unused-format-string-key", # F504 "unused-import", # F401 "unused-variable", # F841 "useless-else-on-loop", # PLW0120 "wildcard-import", # F403 "bad-classmethod-argument", # N804 "consider-iterating-dictionary", # SIM118 "empty-docstring", # D419 "invalid-name", # N815 "line-too-long", # E501, disabled globally "missing-class-docstring", # D101 "missing-final-newline", # W292 "missing-function-docstring", # D103 "missing-module-docstring", # D100 "multiple-imports", #E401 "singleton-comparison", # E711, E712 "subprocess-run-check", # PLW1510 "superfluous-parens", # UP034 "ungrouped-imports", # I001 "unidiomatic-typecheck", # E721 "unnecessary-direct-lambda-call", # PLC3002 "unnecessary-lambda-assignment", # PLC3001 "unnecessary-pass", # PIE790 "unneeded-not", # SIM208 "useless-import-alias", # PLC0414 "wrong-import-order", # I001 "wrong-import-position", # E402 "comparison-of-constants", # PLR0133 "comparison-with-itself", # PLR0124 "consider-alternative-union-syntax", # UP007 "consider-merging-isinstance", # PLR1701 "consider-using-alias", # UP006 "consider-using-dict-comprehension", # C402 "consider-using-generator", # C417 "consider-using-get", # SIM401 "consider-using-set-comprehension", # C401 "consider-using-sys-exit", # PLR1722 "consider-using-ternary", # SIM108 "literal-comparison", # F632 "property-with-parameters", # PLR0206 "super-with-arguments", # UP008 "too-many-branches", # PLR0912 "too-many-return-statements", # PLR0911 "too-many-statements", # PLR0915 "trailing-comma-tuple", # COM818 "unnecessary-comprehension", # C416 "use-a-generator", # C417 "use-dict-literal", # C406 "use-list-literal", # C405 "useless-object-inheritance", # UP004 "useless-return", # PLR1711 "no-else-break", # RET508 "no-else-continue", # RET507 "no-else-raise", # RET506 "no-else-return", # RET505 "broad-except", # BLE001 "protected-access", # SLF001 "broad-exception-raised", # TRY002 "consider-using-f-string", # PLC0209 # "no-self-use", # PLR6301 # Optional plugin, not enabled # Handled by mypy # Ref: "abstract-class-instantiated", "arguments-differ", "assigning-non-slot", "assignment-from-no-return", "assignment-from-none", "bad-exception-cause", "bad-format-character", "bad-reversed-sequence", "bad-super-call", "bad-thread-instantiation", "catching-non-exception", "comparison-with-callable", "deprecated-class", "dict-iter-missing-items", "format-combined-specification", "global-variable-undefined", "import-error", "inconsistent-mro", "inherit-non-class", "init-is-generator", "invalid-class-object", "invalid-enum-extension", "invalid-envvar-value", "invalid-format-returned", "invalid-hash-returned", "invalid-metaclass", "invalid-overridden-method", "invalid-repr-returned", "invalid-sequence-index", "invalid-slice-index", "invalid-slots-object", "invalid-slots", "invalid-star-assignment-target", "invalid-str-returned", "invalid-unary-operand-type", "invalid-unicode-codec", "isinstance-second-argument-not-valid-type", "method-hidden", "misplaced-format-function", "missing-format-argument-key", "missing-format-attribute", "missing-kwoa", "no-member", "no-value-for-parameter", "non-iterator-returned", "non-str-assignment-to-dunder-name", "nonlocal-and-global", "not-a-mapping", "not-an-iterable", "not-async-context-manager", "not-callable", "not-context-manager", "overridden-final-method", "raising-bad-type", "raising-non-exception", "redundant-keyword-arg", "relative-beyond-top-level", "self-cls-assignment", "signature-differs", "star-needs-assignment-target", "subclassed-final-class", "super-without-brackets", "too-many-function-args", "typevar-double-variance", "typevar-name-mismatch", "unbalanced-dict-unpacking", "unbalanced-tuple-unpacking", "unexpected-keyword-arg", "unhashable-member", "unpacking-non-sequence", "unsubscriptable-object", "unsupported-assignment-operation", "unsupported-binary-operation", "unsupported-delete-operation", "unsupported-membership-test", "used-before-assignment", "using-final-decorator-in-unsupported-version", "wrong-exception-operation", ] enable = [ #"useless-suppression", # temporarily every now and then to clean them up "use-symbolic-message-instead", ] per-file-ignores = [ # redefined-outer-name: Tests reference fixtures in the test function # use-implicit-booleaness-not-comparison: Tests need to validate that a list # or a dict is returned "/tests/:redefined-outer-name,use-implicit-booleaness-not-comparison", ] [REPORTS] score = false [TYPECHECK] ignored-classes = [ "_CountingAttr", # for attrs ] mixin-class-rgx = ".*[Mm]ix[Ii]n" [FORMAT] expected-line-ending-format = "LF" [EXCEPTIONS] overgeneral-exceptions = [ "builtins.BaseException", "builtins.Exception", ] [TYPING] runtime-typing = false [CODE_STYLE] max-line-length-suggestions = 72 Qbus-iot-qbusmqttapi-aca6104/.ruff.toml000066400000000000000000000164151503447504100201150ustar00rootroot00000000000000# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml required-version = ">=0.8.0" target-version = "py312" [lint] select = [ "A001", # Variable {name} is shadowing a Python builtin "ASYNC210", # Async functions should not call blocking HTTP methods "ASYNC220", # Async functions should not create subprocesses with blocking methods "ASYNC221", # Async functions should not run processes with blocking methods "ASYNC222", # Async functions should not wait on processes with blocking methods "ASYNC230", # Async functions should not open files with blocking methods like open "ASYNC251", # Async functions should not call time.sleep "B002", # Python does not support the unary prefix increment "B005", # Using .strip() with multi-character strings is misleading "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. "B017", # pytest.raises(BaseException) should be considered evil "B018", # Found useless attribute access. Either assign it to a variable or remove it. "B023", # Function definition does not bind loop variable {name} "B026", # Star-arg unpacking after a keyword argument is strongly discouraged "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? "B904", # Use raise from to specify exception cause "B905", # zip() without an explicit strict= parameter "BLE", "C", # complexity "COM818", # Trailing comma on bare tuple prohibited "D", # docstrings "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) "E", # pycodestyle "F", # pyflakes/autoflake "F541", # f-string without any placeholders "FLY", # flynt "FURB", # refurb "G", # flake8-logging-format "I", # isort "INP", # flake8-no-pep420 "ISC", # flake8-implicit-str-concat "ICN001", # import conventions; {name} should be imported as {asname} "LOG", # flake8-logging "N804", # First argument of a class method should be named cls "N805", # First argument of a method should be named self "N815", # Variable {name} in class scope should not be mixedCase "PERF", # Perflint "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style "PTH", # flake8-pathlib "PYI", # flake8-pyi "RET", # flake8-return "RSE", # flake8-raise "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task "RUF010", # Use explicit conversion flag "RUF013", # PEP 484 prohibits implicit Optional "RUF017", # Avoid quadratic list summation "RUF018", # Avoid assignment expressions in assert statements "RUF019", # Unnecessary key check before dictionary access # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up "S102", # Use of exec detected "S103", # bad-file-permissions "S108", # hardcoded-temp-file "S306", # suspicious-mktemp-usage "S307", # suspicious-eval-usage "S313", # suspicious-xmlc-element-tree-usage "S314", # suspicious-xml-element-tree-usage "S315", # suspicious-xml-expat-reader-usage "S316", # suspicious-xml-expat-builder-usage "S317", # suspicious-xml-sax-usage "S318", # suspicious-xml-mini-dom-usage "S319", # suspicious-xml-pull-dom-usage "S320", # suspicious-xmle-tree-usage "S601", # paramiko-call "S602", # subprocess-popen-with-shell-equals-true "S604", # call-with-shell-equals-true "S608", # hardcoded-sql-expression "S609", # unix-command-wildcard-injection "SIM", # flake8-simplify "SLF", # flake8-self "SLOT", # flake8-slots "T100", # Trace found: {name} used "T20", # flake8-print "TC", # flake8-type-checking "TID", # Tidy imports "TRY", # tryceratops "UP", # pyupgrade "UP031", # Use format specifiers instead of percent format "UP032", # Use f-string instead of `format` call "W", # pycodestyle ] ignore = [ "D202", # No blank lines allowed after function docstring "D203", # 1 blank line required before class docstring "D213", # Multi-line docstring summary should start at the second line "D406", # Section name should end with a newline "D407", # Section name underlining "E501", # line too long "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception "PT018", # Assertion should be broken down into multiple parts "RUF001", # String contains ambiguous unicode character. "RUF002", # Docstring contains ambiguous unicode character. "RUF003", # Comment contains ambiguous unicode character. "RUF015", # Prefer next(...) over single element slice "SIM102", # Use a single if statement instead of nested if statements "SIM103", # Return the condition {condition} directly "SIM108", # Use ternary operator {contents} instead of if-else-block "SIM115", # Use context handler for opening files # Moving imports into type-checking blocks can mess with pytest.patch() "TC001", # Move application import {} into a type-checking block "TC002", # Move third-party import {} into a type-checking block "TC003", # Move standard library import {} into a type-checking block "TRY003", # Avoid specifying long messages outside the exception class "TRY400", # Use `logging.exception` instead of `logging.error` # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", "E114", "E117", "D206", "D300", "Q", "COM812", "COM819", "ISC001", # Disabled because ruff does not understand type of __all__ generated by a function "PLE0605" ] [lint.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false [lint.flake8-tidy-imports.banned-api] "async_timeout".msg = "use asyncio.timeout instead" "pytz".msg = "use zoneinfo instead" [lint.isort] force-sort-within-sections = true known-first-party = [ "pylint", ] combine-as-imports = true split-on-trailing-comma = false [lint.per-file-ignores] # Allow for main entry & scripts to write to stdout "scripts/*" = ["T20"] # Allow relative imports "src/**" = ["TID252"] "tests/**" = ["TID252"] # Temporary #"tests/**" = ["PTH"] [lint.mccabe] max-complexity = 25 Qbus-iot-qbusmqttapi-aca6104/.vscode/000077500000000000000000000000001503447504100175325ustar00rootroot00000000000000Qbus-iot-qbusmqttapi-aca6104/.vscode/settings.json000066400000000000000000000007211503447504100222650ustar00rootroot00000000000000{ "python.testing.pytestArgs": [ "tests" ], // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings "python.testing.pytestEnabled": true, // https://code.visualstudio.com/docs/python/linting#_general-settings "pylint.importStrategy": "fromEnvironment", // "python.analysis.typeCheckingMode": "basic", "python.analysis.diagnosticMode": "workspace", "python.testing.unittestEnabled": false, } Qbus-iot-qbusmqttapi-aca6104/.vscode/tasks.json000066400000000000000000000014431503447504100215540ustar00rootroot00000000000000{ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "label": "Activate virtual environment", "type": "shell", "command": "source .venv/bin/activate" }, { "label": "Build package", "type": "shell", "command": "${command:python.interpreterPath}", "args": ["-m", "build"] }, { "label": "Upload package to test", "type": "shell", "command": "${command:python.interpreterPath}", "args": [ "-m", "twine", "upload", "--repository", "testpypi", "dist/*" ] } ] }Qbus-iot-qbusmqttapi-aca6104/LICENSE000066400000000000000000000020451503447504100171770ustar00rootroot00000000000000MIT License Copyright (c) 2025 Qbus Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Qbus-iot-qbusmqttapi-aca6104/README.md000066400000000000000000000004621503447504100174520ustar00rootroot00000000000000# Qbus MQTT API Python MQTT API for Qbus Home Automation This package is an extention for the Qbus integration in Home Assistant. It includes all convertions from the mqtt protocol which enables Home Assistant to communicate with Qbus Controllers. For more information, please visi https://iot.qbus.be Qbus-iot-qbusmqttapi-aca6104/pyproject.toml000066400000000000000000000012731503447504100211100ustar00rootroot00000000000000[build-system] requires = ["hatchling >= 1.26"] build-backend = "hatchling.build" [project] name = "qbusmqttapi" version = "0.0.0" authors = [ { name="Koen Schockaert", email="ks@qbus.be"}, { name="thomasddn" } ] description = "MQTT API for Qbus Home Automation." readme = "README.md" requires-python = ">=3.12" classifiers = [ "Environment :: Console", "Programming Language :: Python :: 3", "Development Status :: 5 - Production/Stable" ] license = "MIT" license-files = ["LICEN[CS]E*"] dependencies = [ "aiohttp>=3.11.16", "yarl>=1.18.3" ] [project.urls] Homepage = "https://github.com/Qbus-iot/qbusmqttapi" Issues = "https://github.com/Qbus-iot/qbusmqttapi/issues" Qbus-iot-qbusmqttapi-aca6104/pytest.ini000066400000000000000000000003141503447504100202200ustar00rootroot00000000000000[pytest] testpaths = tests asyncio_mode = auto asyncio_default_fixture_loop_scope = function norecursedirs = .git addopts = --cov=src --cov-report=html:tests/coverage --cov-config=.coveragerc Qbus-iot-qbusmqttapi-aca6104/requirements.dev.txt000066400000000000000000000000311503447504100222240ustar00rootroot00000000000000mypy==1.13.0 ruff==0.8.3 Qbus-iot-qbusmqttapi-aca6104/requirements.package.txt000066400000000000000000000000401503447504100230410ustar00rootroot00000000000000build==1.2.2.post1 twine==6.1.0 Qbus-iot-qbusmqttapi-aca6104/requirements.txt000066400000000000000000000000001503447504100214430ustar00rootroot00000000000000Qbus-iot-qbusmqttapi-aca6104/src/000077500000000000000000000000001503447504100167605ustar00rootroot00000000000000Qbus-iot-qbusmqttapi-aca6104/src/qbusmqttapi/000077500000000000000000000000001503447504100213325ustar00rootroot00000000000000Qbus-iot-qbusmqttapi-aca6104/src/qbusmqttapi/__init__.py000066400000000000000000000000251503447504100234400ustar00rootroot00000000000000"""QBUS MQTT API.""" Qbus-iot-qbusmqttapi-aca6104/src/qbusmqttapi/const.py000066400000000000000000000013171503447504100230340ustar00rootroot00000000000000"""Qbus MQTT API constants.""" TOPIC_PREFIX = "cloudapp/QBUSMQTTGW" KEY_OUTPUT_ACTION = "action" KEY_OUTPUT_ACTIONS = "actions" KEY_OUTPUT_ID = "id" KEY_OUTPUT_LOCATION = "location" KEY_OUTPUT_LOCATION_ID = "locationId" KEY_OUTPUT_NAME = "name" KEY_OUTPUT_PROPERTIES = "properties" KEY_OUTPUT_REF_ID = "refId" KEY_OUTPUT_TYPE = "type" KEY_OUTPUT_VARIANT = "variant" KEY_PROPERTIES_AUTHKEY = "authKey" KEY_PROPERTIES_CO2 = "co2" KEY_PROPERTIES_CURRENT_TEMPERATURE = "currTemp" KEY_PROPERTIES_REGIME = "currRegime" KEY_PROPERTIES_SET_TEMPERATURE = "setTemp" KEY_PROPERTIES_SHUTTER_POSITION = "shutterPosition" KEY_PROPERTIES_SLAT_POSITION = "slatPosition" KEY_PROPERTIES_STATE = "state" KEY_PROPERTIES_VALUE = "value" Qbus-iot-qbusmqttapi-aca6104/src/qbusmqttapi/discovery.py000066400000000000000000000114231503447504100237140ustar00rootroot00000000000000"""Qbis discovery models.""" from __future__ import annotations from .const import ( KEY_OUTPUT_ACTIONS, KEY_OUTPUT_ID, KEY_OUTPUT_LOCATION, KEY_OUTPUT_LOCATION_ID, KEY_OUTPUT_NAME, KEY_OUTPUT_PROPERTIES, KEY_OUTPUT_REF_ID, KEY_OUTPUT_TYPE, KEY_OUTPUT_VARIANT, ) KEY_DEVICES = "devices" KEY_DEVICE_FUNCTIONBLOCKS = "functionBlocks" KEY_DEVICE_ID = "id" KEY_DEVICE_IP = "ip" KEY_DEVICE_MAC = "mac" KEY_DEVICE_NAME = "name" KEY_DEVICE_SERIAL_NR = "serialNr" KEY_DEVICE_TYPE = "type" KEY_DEVICE_VERSION = "version" class QbusMqttOutput: """MQTT representation of a Qbus output.""" def __init__(self, data: dict, device: QbusMqttDevice) -> None: """Initialize based on a json loaded dictionary.""" self._data = data self._device = device @property def id(self) -> str: """Return the id.""" return self._data.get(KEY_OUTPUT_ID) or "" @property def type(self) -> str: """Return the type.""" return self._data.get(KEY_OUTPUT_TYPE) or "" @property def name(self) -> str: """Return the name.""" return self._data.get(KEY_OUTPUT_NAME) or "" @property def ref_id(self) -> str: """Return the ref id.""" return self._data.get(KEY_OUTPUT_REF_ID) or "" @property def properties(self) -> dict: """Return the properties.""" return self._data.get(KEY_OUTPUT_PROPERTIES) or {} @property def actions(self) -> dict: """Return the actions.""" return self._data.get(KEY_OUTPUT_ACTIONS) or {} @property def location(self) -> str: """Return the location.""" return self._data.get(KEY_OUTPUT_LOCATION) or "" @property def location_id(self) -> int: """Return the location id.""" return self._data.get(KEY_OUTPUT_LOCATION_ID) or 0 @property def variant(self) -> str | tuple | list: """Return the variant.""" value = self._data.get(KEY_OUTPUT_VARIANT) or "" if isinstance(value, list | tuple): value = [x for x in value if x is not None] return value @property def device(self) -> QbusMqttDevice: """Return the device.""" return self._device class QbusMqttDevice: """MQTT representation of a Qbus device.""" def __init__(self, data: dict) -> None: """Initialize based on a json loaded dictionary.""" self._data = data @property def id(self) -> str: """Return the id.""" return self._data.get(KEY_DEVICE_ID) or "" @property def ip(self) -> str: """Return the ip address.""" return self._data.get(KEY_DEVICE_IP) or "" @property def mac(self) -> str: """Return the ip address.""" return self._data.get(KEY_DEVICE_MAC) or "" @property def name(self) -> str: """Return the ip address.""" return self._data.get(KEY_DEVICE_NAME) or "" @property def serial_number(self) -> str: """Return the serial number.""" return self._data.get(KEY_DEVICE_SERIAL_NR) or "" @property def type(self) -> str: """Return the mac address.""" return self._data.get(KEY_DEVICE_TYPE) or "" @property def version(self) -> str: """Return the version.""" return self._data.get(KEY_DEVICE_VERSION) or "" @property def outputs(self) -> list[QbusMqttOutput]: """Return the outputs.""" outputs: list[QbusMqttOutput] = [] if self._data.get(KEY_DEVICE_FUNCTIONBLOCKS): outputs = [QbusMqttOutput(x, self) for x in self._data[KEY_DEVICE_FUNCTIONBLOCKS]] return outputs class QbusDiscovery: """MQTT representation of a Qbus config.""" def __init__(self, data: dict) -> None: """Initialize based on a json loaded dictionary.""" if KEY_DEVICES in data: self._devices = [QbusMqttDevice(x) for x in data[KEY_DEVICES]] self._name: str = data["app"] def get_device_by_id(self, id: str) -> QbusMqttDevice | None: """Get the device by id.""" return next((x for x in self.devices if x.id.casefold() == id.casefold()), None) def get_device_by_serial(self, serial: str) -> QbusMqttDevice | None: """Get the device by serial.""" return next( (x for x in self.devices if x.serial_number.casefold() == serial.casefold()), None, ) @property def devices(self) -> list[QbusMqttDevice]: """Return the devices.""" return self._devices @property def name(self) -> str: """Return device name.""" return self._name Qbus-iot-qbusmqttapi-aca6104/src/qbusmqttapi/factory.py000066400000000000000000000141401503447504100233530ustar00rootroot00000000000000"""Qbus MQTT factory.""" from dataclasses import dataclass import json import logging from typing import Any, TypeVar from .const import KEY_PROPERTIES_AUTHKEY, TOPIC_PREFIX from .discovery import QbusDiscovery, QbusMqttDevice from .state import ( QbusMqttDeviceState, QbusMqttGatewayState, QbusMqttState, StateAction, StateType, ) _LOGGER = logging.getLogger(__name__) type PublishPayloadType = str | bytes | int | float | None type ReceivePayloadType = str | bytes | bytearray @dataclass class QbusMqttRequestMessage: """Qbus MQTT request data class.""" topic: str payload: PublishPayloadType class QbusMqttMessageFactory: """Factory methods for Qbus MQTT messages.""" T = TypeVar("T", bound="QbusMqttState") def __init__(self) -> None: """Initialize message factory.""" self._topic_factory = QbusMqttTopicFactory() def parse_gateway_state(self, payload: ReceivePayloadType) -> QbusMqttGatewayState | None: """Parse an MQTT message and return an instance of QbusMqttGatewayState if successful, otherwise None.""" return self.deserialize(QbusMqttGatewayState, payload) def parse_discovery(self, payload: ReceivePayloadType) -> QbusDiscovery | None: """Parse an MQTT message and return an instance of QbusDiscovery if successful, otherwise None.""" discovery: QbusDiscovery | None = self.deserialize(QbusDiscovery, payload) # Discovery data must include the Qbus device type and name. if discovery is not None and len(discovery.devices) == 0: _LOGGER.error("Incomplete discovery payload: %s", payload) return None return discovery def parse_device_state(self, payload: ReceivePayloadType) -> QbusMqttDeviceState | None: """Parse an MQTT message and return an instance of QbusMqttDeviceState if successful, otherwise None.""" return self.deserialize(QbusMqttDeviceState, payload) def parse_output_state(self, cls: type[T], payload: ReceivePayloadType) -> T | None: """Parse an MQTT message and return an instance of T if successful, otherwise None.""" return self.deserialize(cls, payload) def create_device_activate_request(self, device: QbusMqttDevice, prefix: str = TOPIC_PREFIX) -> QbusMqttRequestMessage: """Create a message to request device activation.""" state = QbusMqttState(id=device.id, type=StateType.ACTION, action=StateAction.ACTIVATE) state.write_property(KEY_PROPERTIES_AUTHKEY, "ubielite") return QbusMqttRequestMessage( self._topic_factory.get_device_command_topic(device.id, prefix), self.serialize(state), ) def create_device_state_request(self, device: QbusMqttDevice, prefix: str = TOPIC_PREFIX) -> QbusMqttRequestMessage: """Create a message to request a device state.""" return QbusMqttRequestMessage(self._topic_factory.get_get_state_topic(prefix), json.dumps([device.id])) def create_state_request(self, ids: list[str], prefix: str = TOPIC_PREFIX) -> QbusMqttRequestMessage: """Create a message to request states.""" return QbusMqttRequestMessage(self._topic_factory.get_get_state_topic(prefix), json.dumps(ids)) def create_set_output_state_request( self, device: QbusMqttDevice, state: QbusMqttState, prefix: str = TOPIC_PREFIX ) -> QbusMqttRequestMessage: """Create a message to update the output state.""" return QbusMqttRequestMessage( self._topic_factory.get_output_command_topic(device.id, state.id, prefix), self.serialize(state), ) def serialize(self, obj: Any) -> str: """Convert an object to json payload.""" return json.dumps(obj, cls=IgnoreNoneJsonEncoder) def deserialize(self, state_cls: type[Any], payload: ReceivePayloadType) -> Any | None: """Parse an MQTT message and return the requested type if successful, otherwise None.""" if not payload: _LOGGER.warning("Empty state payload for %s", state_cls.__name__) return None try: data = json.loads(payload) except ValueError: _LOGGER.error("Invalid state payload for %s: %s", state_cls.__name__, payload) return None return state_cls(data) class QbusMqttTopicFactory: """Factory methods for topics of the Qbus MQTT API.""" def get_gateway_state_topic(self, prefix: str = TOPIC_PREFIX) -> str: """Return the gateway state topic.""" return f"{prefix}/state" def get_get_config_topic(self, prefix: str = TOPIC_PREFIX) -> str: """Return the getConfig topic.""" return f"{prefix}/getConfig" def get_config_topic(self, prefix: str = TOPIC_PREFIX) -> str: """Return the config topic.""" return f"{prefix}/config" def get_get_state_topic(self, prefix: str = TOPIC_PREFIX) -> str: """Return the getState topic.""" return f"{prefix}/getState" def get_device_state_topic(self, device_id: str, prefix: str = TOPIC_PREFIX) -> str: """Return the state topic.""" return f"{prefix}/{device_id}/state" def get_device_command_topic(self, device_id: str, prefix: str = TOPIC_PREFIX) -> str: """Return the 'set state' topic.""" return f"{prefix}/{device_id}/setState" def get_output_command_topic(self, device_id: str, entity_id: str, prefix: str = TOPIC_PREFIX) -> str: """Return the 'set state' topic of an output.""" return f"{prefix}/{device_id}/{entity_id}/setState" def get_output_state_topic(self, device_id: str, entity_id: str, prefix: str = TOPIC_PREFIX) -> str: """Return the state topic of an output.""" return f"{prefix}/{device_id}/{entity_id}/state" class IgnoreNoneJsonEncoder(json.JSONEncoder): """A json encoder to ignore None values when serializing.""" def default(self, o: Any) -> Any: """Return a serializable object without None values in dictionaries.""" if hasattr(o, "__dict__"): # Filter out None values return {k: v for k, v in o.__dict__.items() if v is not None} return super().default(o) Qbus-iot-qbusmqttapi-aca6104/src/qbusmqttapi/py.typed000066400000000000000000000000001503447504100230170ustar00rootroot00000000000000Qbus-iot-qbusmqttapi-aca6104/src/qbusmqttapi/state.py000066400000000000000000000257721503447504100230410ustar00rootroot00000000000000"""Qbus state models.""" from enum import StrEnum from typing import Any from .const import ( KEY_OUTPUT_ACTION, KEY_OUTPUT_ID, KEY_OUTPUT_PROPERTIES, KEY_OUTPUT_TYPE, KEY_PROPERTIES_CO2, KEY_PROPERTIES_CURRENT_TEMPERATURE, KEY_PROPERTIES_REGIME, KEY_PROPERTIES_SET_TEMPERATURE, KEY_PROPERTIES_SHUTTER_POSITION, KEY_PROPERTIES_SLAT_POSITION, KEY_PROPERTIES_STATE, KEY_PROPERTIES_VALUE, ) KEY_DEVICE_CONNECTABLE = "connectable" KEY_DEVICE_CONNECTED = "connected" KEY_DEVICE_ID = "id" KEY_DEVICE_STATE_PROPERTIES = "properties" KEY_GATEWAY_ID = "id" KEY_GATEWAY_ONLINE = "online" KEY_GATEWAY_REASON = "reason" class StateType(StrEnum): """Values to be used as state type.""" ACTION = "action" EVENT = "event" STATE = "state" class StateAction(StrEnum): """Values to be used as state action.""" ACTIVATE = "activate" ACTIVE = "active" class GaugeStateProperty(StrEnum): """Keys to read gauge state.""" CURRENT_VALUE = "currentValue" CONSUMPTION_VALUE = "consumptionValue" class WeatherStationStateProperty(StrEnum): """Keys to read the weahter station state.""" DAYLIGHT = "dayLight" LIGHT = "light" LIGHT_EAST = "lightEast" LIGHT_SOUTH = "lightSouth" LIGHT_WEST = "lightWest" RAINING = "raining" TEMPERATURE = "temperature" TWILIGHT = "twilight" WIND = "wind" class QbusMqttGatewayState: """MQTT representation of a Qbus gateway state.""" def __init__(self, data: dict) -> None: """Initialize based on a json loaded dictionary.""" self.id: str | None = data.get(KEY_GATEWAY_ID) self.online: bool | None = data.get(KEY_GATEWAY_ONLINE) self.reason: str | None = data.get(KEY_GATEWAY_REASON) class QbusMqttDeviceStateProperties: """MQTT representation of a Qbus device its state properties.""" def __init__(self, data: dict) -> None: """Initialize based on a json loaded dictionary.""" self.connectable: bool | None = data.get(KEY_DEVICE_CONNECTABLE) self.connected: bool | None = data.get(KEY_DEVICE_CONNECTED) class QbusMqttDeviceState: """MQTT representation of a Qbus device state.""" def __init__(self, data: dict) -> None: """Initialize based on a json loaded dictionary.""" self.id: str | None = data.get(KEY_DEVICE_ID) properties = data.get(KEY_DEVICE_STATE_PROPERTIES) self.properties: QbusMqttDeviceStateProperties | None = ( QbusMqttDeviceStateProperties(properties) if properties is not None else None ) class QbusMqttState: """MQTT representation of a Qbus state.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, action: str | None = None, ) -> None: """Initialize state.""" self.id: str = "" self.type: str = "" self.action: str | None = None self.properties: dict | None = None if data is not None: self.id = data.get(KEY_OUTPUT_ID, "") self.type = data.get(KEY_OUTPUT_TYPE, "") self.action = data.get(KEY_OUTPUT_ACTION) self.properties = data.get(KEY_OUTPUT_PROPERTIES) if id is not None: self.id = id if type is not None: self.type = type if action is not None: self.action = action def read_property(self, key: str, default: Any) -> Any: """Read a property.""" return self.properties.get(key, default) if self.properties else default def write_property(self, key: str, value: Any) -> None: """Add or update a property.""" if self.properties is None: self.properties = {} self.properties[key] = value class QbusMqttOnOffState(QbusMqttState): """MQTT representation of a Qbus on/off output.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, ) -> None: """Initialize state.""" super().__init__(data, id=id, type=type) def read_value(self) -> bool: """Read the value of the Qbus output.""" return self.read_property(KEY_PROPERTIES_VALUE, False) def write_value(self, on: bool) -> None: """Set the Qbus output on or off.""" self.write_property(KEY_PROPERTIES_VALUE, on) class QbusMqttAnalogState(QbusMqttState): """MQTT representation of a Qbus analog output.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, ) -> None: """Initialize state.""" super().__init__(data, id=id, type=type) def read_percentage(self) -> float: """Read the value of the Qbus output.""" return self.read_property(KEY_PROPERTIES_VALUE, 0) def write_percentage(self, percentage: float) -> None: """Set the value of the Qbus output.""" self.write_property(KEY_PROPERTIES_VALUE, percentage) def write_on_off(self, on: bool) -> None: """Set the Qbus output on or off.""" self.action = "on" if on else "off" class QbusMqttShutterState(QbusMqttState): """MQTT representation of a Qbus shutter output.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, ) -> None: """Initialize state.""" super().__init__(data, id=id, type=type) def read_state(self) -> str | None: """Read the state of the Qbus output.""" return self.read_property(KEY_PROPERTIES_STATE, None) def write_state(self, state: str) -> None: """Set the state of the Qbus output.""" self.write_property(KEY_PROPERTIES_STATE, state) def read_position(self) -> int | None: """Read the position of the Qbus output.""" return self.read_property(KEY_PROPERTIES_SHUTTER_POSITION, None) def write_position(self, percentage: int) -> None: """Set the position of the Qbus output.""" self.write_property(KEY_PROPERTIES_SHUTTER_POSITION, percentage) def read_slat_position(self) -> int | None: """Read the slat position of the Qbus output.""" return self.read_property(KEY_PROPERTIES_SLAT_POSITION, None) def write_slat_position(self, percentage: int) -> None: """Set the slat position of the Qbus output.""" self.write_property(KEY_PROPERTIES_SLAT_POSITION, percentage) class QbusMqttThermoState(QbusMqttState): """MQTT representation of a Qbus thermo output.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, ) -> None: """Initialize state.""" super().__init__(data, id=id, type=type) def read_current_temperature(self) -> float | None: """Read the current temperature of the Qbus output.""" return self.read_property(KEY_PROPERTIES_CURRENT_TEMPERATURE, None) def read_set_temperature(self) -> float | None: """Read the set temperature of the Qbus output.""" return self.read_property(KEY_PROPERTIES_SET_TEMPERATURE, None) def write_set_temperature(self, temperature: float) -> None: """Set the set temperature of the Qbus output.""" self.write_property(KEY_PROPERTIES_SET_TEMPERATURE, temperature) def read_regime(self) -> str | None: """Read the regime of the Qbus output.""" return self.read_property(KEY_PROPERTIES_REGIME, None) def write_regime(self, regime: str) -> None: """Set the regime of the Qbus output.""" self.write_property(KEY_PROPERTIES_REGIME, regime) class QbusMqttGaugeState(QbusMqttState): """MQTT representation of a Qbus gauge output.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, ) -> None: """Initialize state.""" super().__init__(data, id=id, type=type) def read_value(self, key: GaugeStateProperty) -> float: """Read the value of the Qbus output.""" return self.read_property(key, 0) class QbusMqttVentilationState(QbusMqttState): """MQTT representation of a Qbus ventilation output.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, ) -> None: """Initialize state.""" super().__init__(data, id=id, type=type) def read_co2(self) -> float: """Read the co2 of the Qbus output.""" return self.read_property(KEY_PROPERTIES_CO2, 0) class QbusMqttHumidityState(QbusMqttState): """MQTT representation of a Qbus humidity output.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, ) -> None: """Initialize state.""" super().__init__(data, id=id, type=type) def read_value(self) -> float: """Read the value of the Qbus output.""" return self.read_property(KEY_PROPERTIES_VALUE, 0) class QbusMqttWeatherState(QbusMqttState): """MQTT representation of a Qbus weather station output.""" def __init__( self, data: dict | None = None, *, id: str | None = None, type: str | None = None, ) -> None: """Initialize state.""" super().__init__(data, id=id, type=type) def read_daylight(self) -> float: """Read the daylight status of the weather station.""" return self.read_property(WeatherStationStateProperty.DAYLIGHT, 0) def read_light(self) -> float: """Read the light level of the weather station.""" return self.read_property(WeatherStationStateProperty.LIGHT, 0) def read_light_east(self) -> float: """Read the east-facing light level of the weather station.""" return self.read_property(WeatherStationStateProperty.LIGHT_EAST, 0) def read_light_south(self) -> float: """Read the south-facing light level of the weather station.""" return self.read_property(WeatherStationStateProperty.LIGHT_SOUTH, 0) def read_light_west(self) -> float: """Read the west-facing light level of the weather station.""" return self.read_property(WeatherStationStateProperty.LIGHT_WEST, 0) def read_raining(self) -> bool: """Read the rain status of the weather station.""" return self.read_property(WeatherStationStateProperty.RAINING, False) def read_temperature(self) -> float: """Read the temperature from the weather station.""" return self.read_property(WeatherStationStateProperty.TEMPERATURE, 0) def read_twilight(self) -> bool: """Read the twilight status of the weather station.""" return self.read_property(WeatherStationStateProperty.TWILIGHT, False) def read_wind(self) -> float: """Read the wind speed from the weather station.""" return self.read_property(WeatherStationStateProperty.WIND, 0)