diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index e91980994f..0000000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,4 +0,0 @@ -template: | - ## This release: - - $CHANGES \ No newline at end of file diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index fe96c8fa03..3d931cf8ae 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,30 +1,12 @@ -name: Publish distributions to PyPI and TestPyPI -on: push +name: Publish distributions to PyPI + +on: + release: + types: + - published jobs: - build-and-publish: - name: Build and publish distributions to PyPI and TestPyPI - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.9 - uses: actions/setup-python@v4 - with: - version: 3.9 - - name: Install wheel - run: >- - pip install wheel - - name: Build - run: >- - python3 setup.py sdist bdist_wheel - - name: Publish distribution to Test PyPI - if: startsWith(github.event.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_TEST_TOKEN }} - repository_url: https://test.pypi.org/legacy/ - - name: Publish distribution to PyPI - if: startsWith(github.event.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file + shared-build-and-publish: + uses: zigpy/workflows/.github/workflows/publish-to-pypi.yml@main + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-management.yml b/.github/workflows/release-management.yml deleted file mode 100644 index 2fe7c8237a..0000000000 --- a/.github/workflows/release-management.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Release Management - -on: - push: - branches: - - master - - dev - -jobs: - update_draft_release: - runs-on: ubuntu-latest - steps: - # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v5 - env: - GITHUB_TOKEN: ${{ secrets.GH_RELEASE_DRAFTER }} diff --git a/.hound.yml b/.hound.yml deleted file mode 100644 index 5076d14d77..0000000000 --- a/.hound.yml +++ /dev/null @@ -1,2 +0,0 @@ -python: - enabled: true \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 7b3ea817ea..0000000000 --- a/.isort.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[settings] -# https://github.com/timothycrosley/isort -# https://github.com/timothycrosley/isort/wiki/isort-Settings -# splits long import on multiple lines indented by 4 spaces -profile = black -# by default isort don't check module indexes -not_skip = __init__.py -# will group `import x` and `from x import` of the same module. -force_sort_within_sections = true -sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -default_section = THIRDPARTY -known_first_party = zhaquirks,tests -forced_separate = tests -combine_as_imports = true -use_parentheses = true -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -line_length = 88 -indent = " " \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 919c634c6a..c6988d82d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,8 @@ repos: rev: 6.0.0 hooks: - id: flake8 + additional_dependencies: + - Flake8-pyproject==1.2.3 - repo: https://github.com/PyCQA/isort rev: 5.12.0 @@ -31,6 +33,8 @@ repos: rev: v2.2.4 hooks: - id: codespell + additional_dependencies: + - tomli - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.2.0 diff --git a/README.md b/README.md index 3d5070dece..b7f619aff0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # ZHA Device Handlers For Home Assistant ![CI](https://github.com/zigpy/zha-device-handlers/workflows/CI/badge.svg?branch=dev) -[![Coverage Status](https://coveralls.io/repos/github/zigpy/zha-device-handlers/badge.svg)](https://coveralls.io/github/zigpy/zha-device-handlers) +[![Coverage Status](https://codecov.io/gh/zigpy/zha-device-handlers/branch/dev/graph/badge.svg)](https://codecov.io/gh/zigpy/zha-device-handlers) ZHA Device Handlers are custom quirks implementations for [Zigpy](https://github.com/zigpy/zigpy), the library that provides the [Zigbee](http://www.zigbee.org) support for the [ZHA](https://www.home-assistant.io/components/zha/) component in [Home Assistant](https://www.home-assistant.io). ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices. Zigbee devices that deviate from or do not fully conform to the standard specifications set by the Zigbee Alliance may require the development of custom ZHA Device Handlers (ZHA custom quirks handler implementation) to for all their functions to work properly with the ZHA component in Home Assistant. -Custom quirks implementations for zigpy implemented as ZHA Device Handlers are a similar concept to that of [Hub-connected Device Handlers for the SmartThings Classics platform](https://docs.smartthings.com/en/latest/device-type-developers-guide/) as well that of [Zigbee-Herdsman Converters / Zigbee-Shepherd Converters as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html), meaning they are virtual representation of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms. See [Device Specifics](#Device-Specifics) for details. +Custom quirks implementations for zigpy implemented as ZHA Device Handlers are a similar concept to that of [Hub-connected Device Handlers for the SmartThings Classics platform](https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/) as well that of [Zigbee-Herdsman Converters (formerly Zigbee-Shepherd Converters) as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html), meaning they are virtual representation of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms. See [Device Specifics](#Device-Specifics) for details. # How to contribute @@ -17,15 +17,33 @@ ZHA device handlers and it's provided Quirks allow Zigpy, ZHA and Home Assistant ## What are these specifications -[Zigbee PRO 2017 (R22) Protocol Specification](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf) - -[Zigbee Cluster Library (R8)](https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-08-Zigbee-Cluster-Library.pdf) - -[Zigbee Base Device Behavior Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/zip/zigbee-base-device-behavior-bdb-v1-0.zip) - -[Zigbee Lighting & Occupancy Device Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-15-0014-05-0plo-Lighting-OccupancyDevice-Specification-V1.0.pdf) - -[Zigbee Primer](https://docs.smartthings.com/en/latest/device-type-developers-guide/zigbee-primer.html) +Reference official Zigbee specification documentation from Connectivity Standards Alliance (a.k.a. "CSA-IOT", formerly "Zigbee Alliance"): + +- Zigbee Protocol Specification (also known as "Zigbee Pro" specifications) + - [Zigbee Protocol Specification 2023 (also known as "Zigbee PRO 2023" or just Zigbee R23)](https://csa-iot.org/wp-content/uploads/2023/04/05-3474-23-csg-zigbee-specification-compressed.pdf) + - [Zigbee Protocol Specification 2017 (also known as "Zigbee PRO 2017" or just Zigbee R22)](https://csa-iot.org/wp-content/uploads/2022/01/docs-05-3474-22-0csg-zigbee-specification-1.pdf) + - [Zigbee Protocol Specification 2015 (also known as "Zigbee PRO 2015" or just Zigbee R21)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf) +- Zigbee Cluster Library Specification + - [Zigbee Cluster Library Specification R8 (Revision 8)](https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-08-Zigbee-Cluster-Library.pdf) + - [Zigbee Cluster Library Specification R7 (Revision 7)](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/Zigbee%20Cluster%20Library%20Specification%20v7.pdf) + - [Zigbee Cluster Library Specification R6 (Revision 6)](https://zigbeealliance.org/wp-content/uploads/2019/12/07-5123-06-zigbee-cluster-library-specification.pdf) +- Zigbee Device Specifications + - [Zigbee Base Device Behavior Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/12/docs-13-0402-13-00zi-Base-Device-Behavior-Specification-2-1.pdf) + - [Zigbee Lighting & Occupancy Device Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-15-0014-05-0plo-Lighting-OccupancyDevice-Specification-V1.0.pdf) +- ZigBee Green Power (ZGP "GreenPower" Profile) specifications + - [Zigbee PRO Green Power feature specification Basic functionality set (v 1.1.1)](https://csa-iot.org/wp-content/uploads/2022/01/docs-14-0563-18-batt-Green-Power-Basic-specification-v1.1.1.pdf) + - [Zigbee PRO Green Power feature Specification 1.0a (Revision 26)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-09-5499-26-batt-zigbee-green-power-specification.pdf) +- ZigBee Smart Energy (ZSE / Zigbee SE "Smart Energy" Profile) specifications + - Zigbee Smart Energy Standard 1.4 + - [ZigBee Smart Energy Standard (v1.2a)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-07-5356-19-0zse-zigbee-smart-energy-profile-specification.pdf) + +In additional you can also reference third-party and manufacturer specific documentation: + +- [Tuya - Zigbee Connection Standard (Tuya Smart Documentation)](https://github.com/Koenkk/zigbee-herdsman/blob/master/docs/Zigbee%20Connection%20Standard_Tuya%20Smart_Documentation.pdf) + - [Zigbee2MQTT guide on understanding the custom 'manuSpecificTuya' cluster that TuYa devices uses](https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html) +- [Samsung SmartThings -Device Handlers](https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/) + - [Samsung SmartThings - Zigbee Primer](https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/zigbee-primer.html) + - [Samsung SmartThings - Building ZigBee Device Handlers](https://stdavedemo.readthedocs.io/en/latest/device-type-developers-guide/building-zigbee-device-handlers.html) ## What is a device in human terms diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 8cc5301b82..0000000000 --- a/pylintrc +++ /dev/null @@ -1,58 +0,0 @@ -[MESSAGES CONTROL] -# Reasons disabled: -# 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 -# global-statement - used for the on-demand requirement installation -# redefined-variable-type - this is Python, we're duck typing! -# 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 -# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 -# unnecessary-pass - readability for functions which only contain pass -disable= - format, - abstract-class-little-used, - abstract-method, - cyclic-import, - duplicate-code, - global-statement, - inconsistent-return-statements, - locally-disabled, - not-an-iterable, - not-context-manager, - redefined-variable-type, - too-few-public-methods, - too-many-ancestors, - too-many-arguments, - too-many-branches, - too-many-instance-attributes, - too-many-lines, - too-many-locals, - too-many-public-methods, - too-many-return-statements, - too-many-statements, - unnecessary-pass, - unused-argument - -[REPORTS] -reports=no - -[TYPECHECK] -# For attrs -ignored-classes=_CountingAttr - -[FORMAT] -expected-line-ending-format=LF - -[EXCEPTIONS] -overgeneral-exceptions=Exception - -[BASIC] -good-names=3321S, 3130, 3300S, 3310S, 3305S, 3460L, 3157100, mot003V0, mot003V6 - -[CLASSES] -exclude-protected=_DEVICE_REGISTRY diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..0bfe246aa9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,201 @@ +[build-system] +requires = ["setuptools>=61.0.0", "wheel", "setuptools-git-versioning<2"] +build-backend = "setuptools.build_meta" + +[project] +name = "zha-quirks" +dynamic = ["version"] +description = "Library implementing Zigpy quirks for ZHA in Home Assistant" +urls = {repository = "https://github.com/zigpy/zha-device-handlers"} +authors = [ + {name = "David F. Mulcahey", email = "david.mulcahey@icloud.com"} +] +readme = "README.md" +license = {text = "Apache License Version 2.0"} +requires-python = ">=3.8" +dependencies = [ + "zigpy>=0.56.0", +] + +[tool.setuptools.packages.find] +exclude = ["tests", "tests.*"] + +[project.optional-dependencies] +testing = [ + "pytest", +] + +[tool.setuptools-git-versioning] +enabled = true + +[tool.isort] +# https://github.com/timothycrosley/isort +# https://github.com/timothycrosley/isort/wiki/isort-Settings +# splits long import on multiple lines indented by 4 spaces +profile = "black" +# by default isort don't check module indexes +not_skip = ["__init__.py"] +# will group `import x` and `from x import` of the same module. +force_sort_within_sections = true +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +default_section = "THIRDPARTY" +known_first_party = ["zhaquirks", "tests"] +forced_separate = "tests" +combine_as_imports = true +use_parentheses = true +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +line_length = 88 +indent = " " + +[tool.mypy] +python_version = "3.9" +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_calls = true +disallow_untyped_defs = true +follow_imports = "silent" +ignore_missing_imports = true +no_implicit_optional = true +strict_equality = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true +show_error_codes = true +show_error_context = true +error_summary = true + +install_types = true +non_interactive = true + +disable_error_code = [ + "arg-type", + "assignment", + "attr-defined", + "call-arg", + "dict-item", + "index", + "misc", + "no-any-return", + "no-untyped-call", + "no-untyped-def", + "override", + "return-value", + "union-attr", + "var-annotated", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = "tests" +norecursedirs = ".git testing_config" + +[tool.flake8] +exclude = [".venv", ".git", ".tox", "docs", "venv", "bin", "lib", "deps", "build"] +# To work with Black +max-line-length = 88 +ignore = [ + "E501", # E501: line too long + "W503", # W503: Line break occurred before a binary operator + "E203", # E203: Whitespace before ':' + "D202", # D202 No blank lines allowed after function docstring +] + +[tool.autoflake] +in-place = true +recursive = false +expand-star-imports = false +exclude = [".venv", ".git", ".tox", "docs", "venv", "bin", "lib", "deps", "build"] + +[tool.pydocstyle] +ignore = [ + "D202", + "D203", + "D213", +] + +[tool.pyupgrade] +py37plus = true + +[tool.codespell] +exclude = "pyproject.toml" +ignore-words-list = "hass,dout" +skip = "./.*,test/*" +quiet-level = 2 + +[tool.ruff] +target-version = "py38" + +select = [ + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "ICN001", # import concentions; {name} should be imported as {asname} + "PGH004", # Use specific rule codes when using noqa + "PLC0414", # Useless import alias. Import alias does not rename original package. + "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass + "SIM117", # Merge with-statements that use the same scope + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "SIM201", # Use {left} != {right} instead of not {left} == {right} + "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} + "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. + "SIM401", # Use get from dict with default instead of an if block + "T20", # flake8-print + "TRY004", # Prefer TypeError exception for invalid type + "RUF006", # Store a reference to the return value of asyncio.create_task + "UP", # pyupgrade + "W", # pycodestyle +] + +ignore = [ + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in `__init__` + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D205", # 1 blank line required between summary line and description + "D213", # Multi-line docstring summary should start at the second line + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood: + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D415", # First line should end with a period, question mark, or exclamation point + "E501", # line too long + # the rules below this line should be corrected + "PGH004", # Use specific rule codes when using `noqa` +] + +extend-exclude = [ + "tests" +] + +[tool.ruff.flake8-pytest-style] +fixture-parentheses = false + +[tool.ruff.pyupgrade] +keep-runtime-typing = true + +[tool.ruff.isort] +# will group `import x` and `from x import` of the same module. +force-sort-within-sections = true +known-first-party = [ + "zhaquirks", + "tests", +] +forced-separate = ["tests"] +combine-as-imports = true + +[tool.ruff.mccabe] +max-complexity = 25 \ No newline at end of file diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 178e0c1391..0000000000 --- a/ruff.toml +++ /dev/null @@ -1,71 +0,0 @@ -target-version = "py38" - -select = [ - "B007", # Loop control variable {name} not used within loop body - "B014", # Exception handler with duplicate exception - "C", # complexity - "D", # docstrings - "E", # pycodestyle - "F", # pyflakes/autoflake - "ICN001", # import concentions; {name} should be imported as {asname} - "PGH004", # Use specific rule codes when using noqa - "PLC0414", # Useless import alias. Import alias does not rename original package. - "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass - "SIM117", # Merge with-statements that use the same scope - "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() - "SIM201", # Use {left} != {right} instead of not {left} == {right} - "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} - "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. - "SIM401", # Use get from dict with default instead of an if block - "T20", # flake8-print - "TRY004", # Prefer TypeError exception for invalid type - "RUF006", # Store a reference to the return value of asyncio.create_task - "UP", # pyupgrade - "W", # pycodestyle -] - -ignore = [ - "D100", # Missing docstring in public module - "D101", # Missing docstring in public class - "D102", # Missing docstring in public method - "D103", # Missing docstring in public function - "D104", # Missing docstring in public package - "D105", # Missing docstring in magic method - "D106", # Missing docstring in public nested class - "D107", # Missing docstring in `__init__` - "D202", # No blank lines allowed after function docstring - "D203", # 1 blank line required before class docstring - "D205", # 1 blank line required between summary line and description - "D213", # Multi-line docstring summary should start at the second line - "D400", # First line should end with a period - "D401", # First line of docstring should be in imperative mood: - "D406", # Section name should end with a newline - "D407", # Section name underlining - "D415", # First line should end with a period, question mark, or exclamation point - "E501", # line too long - # the rules below this line should be corrected - "PGH004", # Use specific rule codes when using `noqa` -] - -extend-exclude = [ - "tests" -] - -[flake8-pytest-style] -fixture-parentheses = false - -[pyupgrade] -keep-runtime-typing = true - -[isort] -# will group `import x` and `from x import` of the same module. -force-sort-within-sections = true -known-first-party = [ - "zhaquirks", - "tests", -] -forced-separate = ["tests"] -combine-as-imports = true - -[mccabe] -max-complexity = 25 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 103b7e7e3f..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,100 +0,0 @@ -[tool:pytest] -asyncio_mode = auto -testpaths = tests -norecursedirs = .git testing_config - -[autoflake8] -in-place = True -recursive = False -expand-star-imports = False -exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build - -[flake8] -exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build -# To work with Black -max-line-length = 88 -# E501: line too long -# W503: Line break occurred before a binary operator -# E203: Whitespace before ':' -# D202 No blank lines allowed after function docstring -ignore = - E501, - W503, - E203, - D202 - -[isort] -# https://github.com/timothycrosley/isort -# https://github.com/timothycrosley/isort/wiki/isort-Settings -# splits long import on multiple lines indented by 4 spaces -profile = black -# by default isort don't check module indexes -not_skip = __init__.py -# will group `import x` and `from x import` of the same module. -force_sort_within_sections = true -sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -default_section = THIRDPARTY -known_first_party = zhaquirks,tests -forced_separate = tests -combine_as_imports = true -use_parentheses = true -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -line_length = 88 -indent = " " - -[mypy] -python_version = 3.9 -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_untyped_calls = true -disallow_untyped_defs = true -follow_imports = silent -ignore_missing_imports = true -no_implicit_optional = true -strict_equality = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_return_any = true -warn_unreachable = true -warn_unused_configs = true -warn_unused_ignores = true -show_error_codes = True -show_error_context = True -error_summary = True - -install_types = True -non_interactive = True - -disable_error_code = - arg-type, - assignment, - attr-defined, - call-arg, - dict-item, - index, - misc, - no-any-return, - no-untyped-call, - no-untyped-def, - override, - return-value, - union-attr, - var-annotated - -[pydocstyle] -ignore = - D202, - D203, - D213 - -[pyupgrade] -py37plus = True - -[codespell] -exclude = setup.cfg -ignore-words-list = hass,dout -skip = ./.*,test/* -quiet-level = 2 - diff --git a/setup.py b/setup.py index 5b24b56059..1abbd068c1 100644 --- a/setup.py +++ b/setup.py @@ -1,25 +1,4 @@ -"""Setup module for ZHAQuirks.""" +import setuptools -import pathlib - -from setuptools import find_packages, setup - -VERSION = "0.0.99" - - -setup( - name="zha-quirks", - version=VERSION, - description="Library implementing Zigpy quirks for ZHA in Home Assistant", - long_description=(pathlib.Path(__file__).parent / "README.md").read_text(), - long_description_content_type="text/markdown", - url="https://github.com/dmulcahey/zha-device-handlers", - author="David F. Mulcahey", - author_email="david.mulcahey@icloud.com", - license="Apache License Version 2.0", - keywords="zha quirks homeassistant hass", - packages=find_packages(exclude=["tests"]), - python_requires=">=3.8", - install_requires=["zigpy>=0.53"], - tests_require=["pytest"], -) +if __name__ == "__main__": + setuptools.setup() diff --git a/tests/common.py b/tests/common.py index 9ced1c14ff..dde6ac097c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -15,7 +15,7 @@ def __init__(self, cluster): self.attribute_updates = [] cluster.add_listener(self) - def attribute_updated(self, attr_id, value): + def attribute_updated(self, attr_id, value, timestamp): """Attribute updated listener.""" self.attribute_updates.append((attr_id, value)) diff --git a/tests/test_develco.py b/tests/test_develco.py index b584297ef6..6c58846890 100644 --- a/tests/test_develco.py +++ b/tests/test_develco.py @@ -1,6 +1,31 @@ """Tests for Develco/Frient A/S quirks.""" +import pytest +from zigpy.zcl.clusters.general import DeviceTemperature import zhaquirks.develco.motion +import zhaquirks.develco.power_plug + +from tests.common import ClusterListener + +zhaquirks.setup() + + +@pytest.mark.parametrize("quirk", (zhaquirks.develco.power_plug.SPLZB131,)) +async def test_develco_plug_device_temp_multiplier(zigpy_device_from_quirk, quirk): + """Test device temperature multiplication.""" + + device = zigpy_device_from_quirk(quirk) + + dev_temp_cluster = device.endpoints[2].device_temperature + dev_temp_listener = ClusterListener(dev_temp_cluster) + + dev_temp_attr_id = DeviceTemperature.attributes_by_name["current_temperature"].id + + # turn off heating + dev_temp_cluster._update_attribute(dev_temp_attr_id, 25) + assert len(dev_temp_listener.attribute_updates) == 1 + assert dev_temp_listener.attribute_updates[0][0] == dev_temp_attr_id + assert dev_temp_listener.attribute_updates[0][1] == 2500 # multiplied by 100 def test_motion_signature(assert_signature_matches_quirk): diff --git a/tests/test_ikea.py b/tests/test_ikea.py index 37126d9a46..397ef523a4 100644 --- a/tests/test_ikea.py +++ b/tests/test_ikea.py @@ -1,7 +1,15 @@ """Tests for Ikea Starkvind quirks.""" +from unittest import mock + +from zigpy.zcl import foundation +from zigpy.zcl.clusters.measurement import PM25 + +import zhaquirks import zhaquirks.ikea.starkvind +zhaquirks.setup() + def test_ikea_starkvind(assert_signature_matches_quirk): """Test new 'STARKVIND Air purifier table' signature is matched to its quirk.""" @@ -72,3 +80,45 @@ def test_ikea_starkvind_v2(assert_signature_matches_quirk): } assert_signature_matches_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND_v2, signature) + + +async def test_pm25_cluster_read(zigpy_device_from_quirk): + """Test reading from PM25 cluster""" + + starkvind_device = zigpy_device_from_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND) + assert starkvind_device.model == "STARKVIND Air purifier" + + pm25_cluster = starkvind_device.endpoints[1].in_clusters[PM25.cluster_id] + ikea_cluster = starkvind_device.endpoints[1].in_clusters[ + zhaquirks.ikea.starkvind.IkeaAirpurifier.cluster_id + ] + + # Mock the read attribute to on the IkeaAirpurifier cluster + # to always return 6 for anything. + def mock_read(attributes, manufacturer=None): + records = [ + foundation.ReadAttributeRecord( + attr, foundation.Status.SUCCESS, foundation.TypeValue(None, 6) + ) + for attr in attributes + ] + return (records,) + + patch_ikeacluster_read = mock.patch.object( + ikea_cluster, "_read_attributes", mock.AsyncMock(side_effect=mock_read) + ) + with patch_ikeacluster_read: + # Reading "measured_value" should read the "air_quality_25pm" value from + # the IkeaAirpurifier cluster + success, fail = await pm25_cluster.read_attributes(["measured_value"]) + assert success + assert 6 in success.values() + assert not fail + + # Same call with allow_cache=True; a bug previously prevented this from working + success, fail = await pm25_cluster.read_attributes( + ["measured_value"], allow_cache=True + ) + assert success + assert 6 in success.values() + assert not fail diff --git a/tests/test_legrand.py b/tests/test_legrand.py index 952e1515af..ab98c25f91 100644 --- a/tests/test_legrand.py +++ b/tests/test_legrand.py @@ -26,3 +26,31 @@ async def test_legrand_battery(zigpy_device_from_quirk, voltage, bpr): power_cluster = device.endpoints[1].power power_cluster.update_attribute(0x0020, voltage) assert power_cluster["battery_percentage_remaining"] == bpr + + +def test_light_switch_with_neutral_signature(assert_signature_matches_quirk): + signature = { + "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=1, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4129, maximum_buffer_size=89, maximum_incoming_transfer_size=63, server_mask=10752, maximum_outgoing_transfer_size=63, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)", + "endpoints": { + "1": { + "profile_id": 260, + "device_type": "0x0100", + "in_clusters": [ + "0x0000", + "0x0003", + "0x0004", + "0x0005", + "0x0006", + "0x000f", + "0xfc01", + ], + "out_clusters": ["0x0000", "0x0019", "0xfc01"], + } + }, + "manufacturer": " Legrand", + "model": " Light switch with neutral", + "class": "zigpy.device.Device", + } + assert_signature_matches_quirk( + zhaquirks.legrand.switch.LightSwitchWithNeutral, signature + ) diff --git a/tests/test_thirdreality.py b/tests/test_thirdreality.py new file mode 100644 index 0000000000..0d2af260d1 --- /dev/null +++ b/tests/test_thirdreality.py @@ -0,0 +1,40 @@ +"""Tests for Third Reality quirks.""" + + +import pytest +from zigpy.zcl.clusters.security import IasZone + +import zhaquirks +import zhaquirks.thirdreality.night_light + +from tests.common import ClusterListener + +zhaquirks.setup() + + +@pytest.mark.parametrize("quirk", (zhaquirks.thirdreality.night_light.Nightlight,)) +async def test_third_reality_nightlight(zigpy_device_from_quirk, quirk): + """Test Third Reality night light forwarding motion attribute to IasZone cluster.""" + + device = zigpy_device_from_quirk(quirk) + + ias_zone_cluster = device.endpoints[1].ias_zone + ias_zone_listener = ClusterListener(ias_zone_cluster) + + ias_zone_status_id = IasZone.AttributeDefs.zone_status.id + + third_reality_cluster = device.endpoints[1].in_clusters[0xFC00] + + # 0x0002 is also used on manufacturer specific cluster for motion events + third_reality_cluster.update_attribute(0x0002, IasZone.ZoneStatus.Alarm_1) + + assert len(ias_zone_listener.attribute_updates) == 1 + assert ias_zone_listener.attribute_updates[0][0] == ias_zone_status_id + assert ias_zone_listener.attribute_updates[0][1] == IasZone.ZoneStatus.Alarm_1 + + # turn off motion alarm + third_reality_cluster.update_attribute(0x0002, 0) + + assert len(ias_zone_listener.attribute_updates) == 2 + assert ias_zone_listener.attribute_updates[1][0] == ias_zone_status_id + assert ias_zone_listener.attribute_updates[1][1] == 0 diff --git a/tests/test_tuya.py b/tests/test_tuya.py index 4316df6b91..babf1ddf73 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -32,6 +32,7 @@ ON, OUTPUT_CLUSTERS, PROFILE_ID, + SKIP_CONFIGURATION, ZONE_STATUS_CHANGE_COMMAND, ) from zhaquirks.tuya import ( @@ -1408,6 +1409,25 @@ def test_ts0601_valve_signature(assert_signature_matches_quirk): assert_signature_matches_quirk(zhaquirks.tuya.ts0601_valve.TuyaValve, signature) +def test_ts0601_motion_signature(assert_signature_matches_quirk): + """Test TS0601 motion by TreatLife remote signature is matched to its quirk.""" + signature = { + "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)", + "endpoints": { + "1": { + "profile_id": 260, + "device_type": "0x0051", + "in_clusters": ["0x0000", "0x0004", "0x0005", "0xef00"], + "out_clusters": ["0x000a", "0x0019"], + } + }, + "manufacturer": "_TZE200_ppuj1vem", + "model": "TS0601", + "class": "zigpy.device.Device", + } + assert_signature_matches_quirk(zhaquirks.tuya.ts0601_motion.NeoMotion, signature) + + def test_multiple_attributes_report(): """Test a multi attribute report from Tuya device.""" @@ -1620,6 +1640,13 @@ async def test_tuya_spell(zigpy_device_from_quirk): for quirk in ENCHANTED_QUIRKS: device = zigpy_device_from_quirk(quirk) + # fail if SKIP_CONFIGURATION is set, as that will cause ZHA to not call bind() + if getattr(device, SKIP_CONFIGURATION, False): + pytest.fail( + f"Enchanted quirk {quirk} has SKIP_CONFIGURATION set. " + f"This is not allowed for enchanted devices." + ) + for cluster in itertools.chain( device.endpoints[1].in_clusters.values(), device.endpoints[1].out_clusters.values(), diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 4faf9ec5ff..07391ce90f 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -1,14 +1,29 @@ """Tests for xiaomi.""" import asyncio +import logging +import math from unittest import mock import pytest import zigpy.device import zigpy.types as t from zigpy.zcl import foundation -from zigpy.zcl.clusters.general import PowerConfiguration +from zigpy.zcl.clusters.general import ( + AnalogInput, + DeviceTemperature, + PowerConfiguration, +) +from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.hvac import Thermostat +from zigpy.zcl.clusters.measurement import ( + IlluminanceMeasurement, + OccupancySensing, + PressureMeasurement, + RelativeHumidity, + TemperatureMeasurement, +) from zigpy.zcl.clusters.security import IasZone +from zigpy.zcl.clusters.smartenergy import Metering import zhaquirks from zhaquirks.const import ( @@ -25,10 +40,9 @@ ZONE_STATUS_CHANGE_COMMAND, ) from zhaquirks.xiaomi import ( - CONSUMPTION_REPORTED, LUMI, - POWER_REPORTED, - VOLTAGE_REPORTED, + XIAOMI_AQARA_ATTRIBUTE, + XIAOMI_AQARA_ATTRIBUTE_E1, XIAOMI_NODE_DESC, BasicCluster, XiaomiCustomDevice, @@ -51,11 +65,16 @@ AqaraFeederAcn001, OppleCluster, ) +import zhaquirks.xiaomi.aqara.motion_ac02 import zhaquirks.xiaomi.aqara.motion_aq2 import zhaquirks.xiaomi.aqara.motion_aq2b +import zhaquirks.xiaomi.aqara.plug import zhaquirks.xiaomi.aqara.plug_eu +import zhaquirks.xiaomi.aqara.roller_curtain_e1 import zhaquirks.xiaomi.aqara.smoke from zhaquirks.xiaomi.aqara.thermostat_agl001 import ScheduleEvent, ScheduleSettings +import zhaquirks.xiaomi.aqara.switch_t1 +import zhaquirks.xiaomi.aqara.weather import zhaquirks.xiaomi.mija.motion from tests.common import ZCL_OCC_ATTR_RPT_OCC, ClusterListener @@ -63,6 +82,15 @@ zhaquirks.setup() +def create_aqara_attr_report(attributes): + """Creates a special Aqara attriubte report with t.Single as a type for all values.""" + serialized_data = b"" + for key, value in attributes.items(): + tv = foundation.TypeValue(0x39, t.Single(value)) # mostly used + serialized_data += bytes([key]) + tv.serialize() + return serialized_data + + def test_basic_cluster_deserialize_wrong_len(): """Test attr report with model and xiaomi attr.""" cluster = BasicCluster(mock.MagicMock()) @@ -489,46 +517,72 @@ async def test_xiaomi_eu_plug_binding(zigpy_device_from_quirk, quirk): ) -@pytest.mark.parametrize("quirk", (zhaquirks.xiaomi.aqara.plug_eu.PlugMAEU01,)) -async def test_xiaomi_eu_plug_power(zigpy_device_from_quirk, quirk): - """Test current power consumption, total power consumption, and current voltage on Xiaomi EU plug.""" +@pytest.mark.parametrize( + "quirk", + ( + zhaquirks.xiaomi.aqara.plug_eu.PlugMAEU01, + zhaquirks.xiaomi.aqara.switch_t1.SwitchT1, + ), +) +async def test_xiaomi_plug_power(zigpy_device_from_quirk, quirk): + """Test current power consumption, total power consumption, and current voltage on Xiaomi EU plug and T1 relay.""" device = zigpy_device_from_quirk(quirk) + basic_cluster = device.endpoints[1].basic em_cluster = device.endpoints[1].electrical_measurement em_listener = ClusterListener(em_cluster) # Test voltage on ElectricalMeasurement cluster - em_cluster.endpoint.device.voltage_bus.listener_event(VOLTAGE_REPORTED, 230) + zcl_em_voltage = ElectricalMeasurement.AttributeDefs.rms_voltage.id + basic_cluster.update_attribute( + XIAOMI_AQARA_ATTRIBUTE, create_aqara_attr_report({150: 2300}) + ) assert len(em_listener.attribute_updates) == 1 - assert em_listener.attribute_updates[0][0] == 1285 + assert em_listener.attribute_updates[0][0] == zcl_em_voltage assert em_listener.attribute_updates[0][1] == 230 # Test current power consumption on ElectricalMeasurement cluster - em_cluster.endpoint.device.power_bus.listener_event(POWER_REPORTED, 15) + zcl_em_current_power = ElectricalMeasurement.AttributeDefs.active_power.id + basic_cluster.update_attribute( + XIAOMI_AQARA_ATTRIBUTE, create_aqara_attr_report({152: 15}) + ) assert len(em_listener.attribute_updates) == 2 - assert em_listener.attribute_updates[1][0] == 1291 + assert em_listener.attribute_updates[1][0] == zcl_em_current_power assert em_listener.attribute_updates[1][1] == 150 # multiplied by 10 - # Test total power consumption on ElectricalMeasurement cluster - em_cluster.endpoint.device.consumption_bus.listener_event( - CONSUMPTION_REPORTED, 0.001 - ) - assert len(em_listener.attribute_updates) == 3 - assert em_listener.attribute_updates[2][0] == 772 - assert em_listener.attribute_updates[2][1] == 1 # multiplied by 1000 - - # Test total power consumption on SmartEnergy cluster + # Test total power consumption on ElectricalMeasurement cluster and SmartEnergy cluster + zcl_em_total_power = ElectricalMeasurement.AttributeDefs.total_active_power.id + zcl_se_total_power = Metering.AttributeDefs.current_summ_delivered.id se_cluster = device.endpoints[1].smartenergy_metering se_listener = ClusterListener(se_cluster) - se_cluster.endpoint.device.consumption_bus.listener_event( - CONSUMPTION_REPORTED, 0.001 + basic_cluster.update_attribute( + XIAOMI_AQARA_ATTRIBUTE, create_aqara_attr_report({149: 0.001}) ) + # electrical measurement cluster + assert len(em_listener.attribute_updates) == 3 + assert em_listener.attribute_updates[2][0] == zcl_em_total_power + assert em_listener.attribute_updates[2][1] == 1 # multiplied by 1000 + + # smart energy cluster assert len(se_listener.attribute_updates) == 1 - assert se_listener.attribute_updates[0][0] == 0 + assert se_listener.attribute_updates[0][0] == zcl_se_total_power assert se_listener.attribute_updates[0][1] == 1 # multiplied by 1000 + # test current power consumption attribute report on AnalogInput is forwarded to ElectricalMeasurement + analog_input_cluster = device.endpoints[21].analog_input + analog_input_listener = ClusterListener(analog_input_cluster) + zcl_analog_input_value = AnalogInput.AttributeDefs.present_value.id + + analog_input_cluster.update_attribute(zcl_analog_input_value, 40) + assert len(analog_input_listener.attribute_updates) == 1 + assert analog_input_listener.attribute_updates[0][0] == zcl_analog_input_value + assert analog_input_listener.attribute_updates[0][1] == 40 + + assert em_listener.attribute_updates[3][0] == zcl_em_current_power + assert em_listener.attribute_updates[3][1] == 400 # multiplied by 10 + @pytest.mark.parametrize( "attribute, value, expected_bytes", @@ -582,88 +636,96 @@ async def test_aqara_feeder_write_attrs( b"\x1c_\x11f\n\xf1\xffA\t\x00\x05\x01\x04\x15\x00U\x01\x01", 2, [ - mock.call(ZCL_FEEDING, True), - mock.call(FEEDER_ATTR, b"\x00\x05\x01\x04\x15\x00U\x01\x01"), + mock.call(ZCL_FEEDING, True, mock.ANY), + mock.call(FEEDER_ATTR, b"\x00\x05\x01\x04\x15\x00U\x01\x01", mock.ANY), ], ), ( b"\x1c_\x11l\n\xf1\xffA\x0c\x00\x05\xd0\x04\x15\x02\xbc\x040203", 3, [ - mock.call(ZCL_LAST_FEEDING_SIZE, 3), - mock.call(ZCL_LAST_FEEDING_SOURCE, OppleCluster.FeedingSource.Remote), - mock.call(FEEDER_ATTR, b"\x00\x05\xd0\x04\x15\x02\xbc\x040203"), + mock.call(ZCL_LAST_FEEDING_SIZE, 3, mock.ANY), + mock.call( + ZCL_LAST_FEEDING_SOURCE, OppleCluster.FeedingSource.Remote, mock.ANY + ), + mock.call( + FEEDER_ATTR, b"\x00\x05\xd0\x04\x15\x02\xbc\x040203", mock.ANY + ), ], ), ( b"\x1c_\x11m\n\xf1\xffA\n\x00\x05\xd1\rh\x00U\x02\x00!", 2, [ - mock.call(ZCL_PORTIONS_DISPENSED, 33), - mock.call(FEEDER_ATTR, b"\x00\x05\xd1\rh\x00U\x02\x00!"), + mock.call(ZCL_PORTIONS_DISPENSED, 33, mock.ANY), + mock.call(FEEDER_ATTR, b"\x00\x05\xd1\rh\x00U\x02\x00!", mock.ANY), ], ), ( b"\x1c_\x11n\n\xf1\xffA\x0c\x00\x05\xd2\ri\x00U\x04\x00\x00\x01\x08", 2, [ - mock.call(ZCL_WEIGHT_DISPENSED, 264), - mock.call(FEEDER_ATTR, b"\x00\x05\xd2\ri\x00U\x04\x00\x00\x01\x08"), + mock.call(ZCL_WEIGHT_DISPENSED, 264, mock.ANY), + mock.call( + FEEDER_ATTR, b"\x00\x05\xd2\ri\x00U\x04\x00\x00\x01\x08", mock.ANY + ), ], ), ( b"\x1c_\x11o\n\xf1\xffA\t\x00\x05\xd3\r\x0b\x00U\x01\x00", 2, [ - mock.call(ZCL_ERROR_DETECTED, False), - mock.call(FEEDER_ATTR, b"\x00\x05\xd3\r\x0b\x00U\x01\x00"), + mock.call(ZCL_ERROR_DETECTED, False, mock.ANY), + mock.call(FEEDER_ATTR, b"\x00\x05\xd3\r\x0b\x00U\x01\x00", mock.ANY), ], ), ( b"\x1c_\x11p\n\xf1\xffA\t\x00\x05\x05\x04\x16\x00U\x01\x01", 2, [ - mock.call(ZCL_CHILD_LOCK, True), - mock.call(FEEDER_ATTR, b"\x00\x05\x05\x04\x16\x00U\x01\x01"), + mock.call(ZCL_CHILD_LOCK, True, mock.ANY), + mock.call(FEEDER_ATTR, b"\x00\x05\x05\x04\x16\x00U\x01\x01", mock.ANY), ], ), ( b"\x1c_\x11r\n\xf1\xffA\t\x00\x05\t\x04\x17\x00U\x01\x01", 2, [ - mock.call(ZCL_DISABLE_LED_INDICATOR, True), - mock.call(FEEDER_ATTR, b"\x00\x05\t\x04\x17\x00U\x01\x01"), + mock.call(ZCL_DISABLE_LED_INDICATOR, True, mock.ANY), + mock.call(FEEDER_ATTR, b"\x00\x05\t\x04\x17\x00U\x01\x01", mock.ANY), ], ), ( b"\x1c_\x11s\n\xf1\xffA\t\x00\x05\x0b\x04\x18\x00U\x01\x01", 2, [ - mock.call(ZCL_FEEDING_MODE, OppleCluster.FeedingMode.Schedule), - mock.call(FEEDER_ATTR, b"\x00\x05\x0b\x04\x18\x00U\x01\x01"), + mock.call( + ZCL_FEEDING_MODE, OppleCluster.FeedingMode.Schedule, mock.ANY + ), + mock.call(FEEDER_ATTR, b"\x00\x05\x0b\x04\x18\x00U\x01\x01", mock.ANY), ], ), ( b"\x1c_\x11u\n\xf1\xffA\t\x00\x05\x0f\x0e_\x00U\x01\x06", 2, [ - mock.call(ZCL_PORTION_WEIGHT, 6), - mock.call(FEEDER_ATTR, b"\x00\x05\x0f\x0e_\x00U\x01\x06"), + mock.call(ZCL_PORTION_WEIGHT, 6, mock.ANY), + mock.call(FEEDER_ATTR, b"\x00\x05\x0f\x0e_\x00U\x01\x06", mock.ANY), ], ), ( b"\x1c_\x11v\n\xf1\xffA\t\x00\x05\x11\x0e\\\x00U\x01\x02", 2, [ - mock.call(ZCL_SERVING_SIZE, 2), - mock.call(FEEDER_ATTR, b"\x00\x05\x11\x0e\\\x00U\x01\x02"), + mock.call(ZCL_SERVING_SIZE, 2, mock.ANY), + mock.call(FEEDER_ATTR, b"\x00\x05\x11\x0e\\\x00U\x01\x02", mock.ANY), ], ), ( b"\x1c_\x11{\n\xf7\x00A\x0e\x05!\x0e\x00\r#!%\x00\x00\t!\x02\x03", 1, [ - mock.call(0x00F7, b"\x05!\x0e\x00\r#!%\x00\x00\t!\x02\x03"), + mock.call(0x00F7, b"\x05!\x0e\x00\r#!%\x00\x00\t!\x02\x03", mock.ANY), ], ), ( @@ -673,6 +735,7 @@ async def test_aqara_feeder_write_attrs( mock.call( FEEDER_ATTR, b"\x00\x05\x15\x08\x00\x08\xc8 7F09000100,7F0D000100,7F13000100", + mock.ANY, ), ], ), @@ -716,26 +779,26 @@ async def test_aqara_smoke_sensor_attribute_update(zigpy_device_from_quirk, quir ias_cluster = device.endpoints[1].ias_zone ias_listener = ClusterListener(ias_cluster) - zone_status_id = IasZone.attributes_by_name["zone_status"].id + zone_status_id = IasZone.AttributeDefs.zone_status.id # check that updating Xiaomi smoke attribute also updates zone status on the Ias Zone cluster # turn on smoke alarm - opple_cluster._update_attribute(0x013A, 1) + opple_cluster.update_attribute(0x013A, 1) assert len(opple_listener.attribute_updates) == 1 assert len(ias_listener.attribute_updates) == 1 assert ias_listener.attribute_updates[0][0] == zone_status_id assert ias_listener.attribute_updates[0][1] == IasZone.ZoneStatus.Alarm_1 # turn off smoke alarm - opple_cluster._update_attribute(0x013A, 0) + opple_cluster.update_attribute(0x013A, 0) assert len(opple_listener.attribute_updates) == 2 assert len(ias_listener.attribute_updates) == 2 assert ias_listener.attribute_updates[1][0] == zone_status_id assert ias_listener.attribute_updates[1][1] == 0 # check if fake dB/m smoke density attribute is also updated - opple_cluster._update_attribute(0x013B, 10) + opple_cluster.update_attribute(0x013B, 10) assert len(opple_listener.attribute_updates) == 4 assert opple_listener.attribute_updates[2][0] == 0x013B assert opple_listener.attribute_updates[2][1] == 10 @@ -781,10 +844,7 @@ async def test_aqara_smoke_sensor_xiaomi_attribute_report( # check that Xiaomi attribute report resets smoke zone status assert len(ias_listener.attribute_updates) == 1 - assert ( - ias_listener.attribute_updates[0][0] - == IasZone.attributes_by_name["zone_status"].id - ) + assert ias_listener.attribute_updates[0][0] == IasZone.AttributeDefs.zone_status.id assert ias_listener.attribute_updates[0][1] == expected_zone_status @@ -793,8 +853,8 @@ async def test_aqara_smoke_sensor_xiaomi_attribute_report( [ ("system_mode", "unoccupied_heating_setpoint"), ( - Thermostat.attributes_by_name["system_mode"].id, - Thermostat.attributes_by_name["unoccupied_heating_setpoint"].id, + Thermostat.AttributeDefs.system_mode.id, + Thermostat.AttributeDefs.unoccupied_heating_setpoint.id, ), ], ) @@ -866,7 +926,7 @@ def mock_read(attributes, manufacturer=None): 0x0271 ] # Opple system_mode attribute assert thermostat_listener.attribute_updates[0] == ( - Thermostat.attributes_by_name["system_mode"].id, + Thermostat.AttributeDefs.system_mode.id, Thermostat.SystemMode.Heat, ) # check that attributes are correctly mapped and updated on ZCL thermostat cluster @@ -895,7 +955,7 @@ def mock_read(attributes, manufacturer=None): assert opple_listener.attribute_updates[1] == (0x0271, 1) # Opple system_mode assert thermostat_listener.attribute_updates[2] == ( - Thermostat.attributes_by_name["system_mode"].id, + Thermostat.AttributeDefs.system_mode.id, Thermostat.SystemMode.Heat, ) # check ZCL attribute is in correct mode @@ -924,29 +984,29 @@ async def test_xiaomi_e1_thermostat_attribute_update(zigpy_device_from_quirk, qu power_config_cluster = device.endpoints[1].power power_config_listener = ClusterListener(power_config_cluster) - zcl_system_mode_id = Thermostat.attributes_by_name["system_mode"].id - zcl_battery_percentage_id = PowerConfiguration.attributes_by_name[ - "battery_percentage_remaining" - ].id + zcl_system_mode_id = Thermostat.AttributeDefs.system_mode.id + zcl_battery_percentage_id = ( + PowerConfiguration.AttributeDefs.battery_percentage_remaining.id + ) # check that updating Xiaomi system_mode also updates an attribute on the Thermostat cluster # turn off heating - opple_cluster._update_attribute(0x0271, 0) + opple_cluster.update_attribute(0x0271, 0) assert len(opple_listener.attribute_updates) == 1 assert len(thermostat_listener.attribute_updates) == 1 assert thermostat_listener.attribute_updates[0][0] == zcl_system_mode_id assert thermostat_listener.attribute_updates[0][1] == Thermostat.SystemMode.Off # turn on heating - opple_cluster._update_attribute(0x0271, 1) + opple_cluster.update_attribute(0x0271, 1) assert len(opple_listener.attribute_updates) == 2 assert len(thermostat_listener.attribute_updates) == 2 assert thermostat_listener.attribute_updates[1][0] == zcl_system_mode_id assert thermostat_listener.attribute_updates[1][1] == Thermostat.SystemMode.Heat # check that updating battery_percentage on the OppleCluster also updates the PowerConfiguration cluster - opple_cluster._update_attribute(0x040A, 50) # 50% battery + opple_cluster.update_attribute(0x040A, 50) # 50% battery assert len(opple_listener.attribute_updates) == 3 assert len(power_config_listener.attribute_updates) == 1 assert power_config_listener.attribute_updates[0][0] == zcl_battery_percentage_id @@ -1051,3 +1111,276 @@ async def test_xiaomi_e1_thermostat_schedule_settings_deserialization( s = ScheduleSettings(schedule_settings) assert str(s) == expected_string + + +@pytest.mark.parametrize("quirk", (zhaquirks.xiaomi.aqara.motion_ac02.LumiMotionAC02,)) +async def test_xiaomi_p1_motion_sensor(zigpy_device_from_quirk, quirk): + """Test Aqara P1 motion sensor.""" + + device = zigpy_device_from_quirk(quirk) + + opple_cluster = device.endpoints[1].opple_cluster + opple_listener = ClusterListener(opple_cluster) + + ias_cluster = device.endpoints[1].ias_zone + ias_listener = ClusterListener(ias_cluster) + + occupancy_cluster = device.endpoints[1].occupancy + occupancy_listener = ClusterListener(occupancy_cluster) + + illuminance_cluster = device.endpoints[1].illuminance + illuminance_listener = ClusterListener(illuminance_cluster) + + zcl_zone_status_change_cmd_id = ( + IasZone.ClientCommandDefs.status_change_notification.id + ) + zcl_occupancy_id = OccupancySensing.AttributeDefs.occupancy.id + zcl_iilluminance_id = IlluminanceMeasurement.AttributeDefs.measured_value.id + + # send motion and illuminance report 10 + opple_cluster.update_attribute(274, 10 + 65536) + + # confirm manufacturer specific attribute report + assert len(opple_listener.attribute_updates) == 1 + assert opple_listener.attribute_updates[0][0] == 274 + assert opple_listener.attribute_updates[0][1] == 10 + 65536 + + # confirm zone status change notification command + assert len(ias_listener.cluster_commands) == 1 + assert ias_listener.cluster_commands[0][1] == zcl_zone_status_change_cmd_id + assert ias_listener.cluster_commands[0][2][0] == IasZone.ZoneStatus.Alarm_1 + + # confirm occupancy report + assert len(occupancy_listener.attribute_updates) == 1 + assert occupancy_listener.attribute_updates[0][0] == zcl_occupancy_id + assert ( + occupancy_listener.attribute_updates[0][1] + == OccupancySensing.Occupancy.Occupied + ) + + # confirm illuminance report (with conversion) + assert len(illuminance_listener.attribute_updates) == 1 + assert illuminance_listener.attribute_updates[0][0] == zcl_iilluminance_id + assert illuminance_listener.attribute_updates[0][1] == 10000 * math.log10(10) + 1 + + # send invalid illuminance report 0xFFFF (and motion) + opple_cluster.update_attribute(274, 0xFFFF) + + # confirm invalid illuminance report is interpreted as 0 + assert len(illuminance_listener.attribute_updates) == 2 + assert illuminance_listener.attribute_updates[1][0] == zcl_iilluminance_id + assert illuminance_listener.attribute_updates[1][1] == 0 + + # send illuminance report only + opple_cluster.update_attribute( + XIAOMI_AQARA_ATTRIBUTE_E1, create_aqara_attr_report({101: 20}) + ) + assert len(illuminance_listener.attribute_updates) == 3 + assert illuminance_listener.attribute_updates[2][0] == zcl_iilluminance_id + assert illuminance_listener.attribute_updates[2][1] == 10000 * math.log10(20) + 1 + + +@pytest.mark.parametrize( + "raw_report, expected_results", + ( + [ + "18200A01FF412501214F0B0421A84305214E020624010000000064299B096521BE1B662B138D01000A21900D", + [ + 2459, # temperature + 7102, # humidity + 1016.51, # pressure + 28.9, # battery voltage + 54, # battery percent * 2 + ], + ], + ), +) +async def test_xiaomi_weather(zigpy_device_from_quirk, raw_report, expected_results): + """Test Aqara weather sensor.""" + raw_report = bytes.fromhex(raw_report) + + device = zigpy_device_from_quirk(zhaquirks.xiaomi.aqara.weather.Weather2) + + basic_cluster = device.endpoints[1].basic + + temperature_cluster = device.endpoints[1].temperature + temperature_listener = ClusterListener(temperature_cluster) + + humidity_cluster = device.endpoints[1].humidity + humidity_listener = ClusterListener(humidity_cluster) + + pressure_cluster = device.endpoints[1].pressure + pressure_listener = ClusterListener(pressure_cluster) + + power_cluster = device.endpoints[1].power + power_listener = ClusterListener(power_cluster) + + zcl_temperature_id = TemperatureMeasurement.AttributeDefs.measured_value.id + zcl_humidity_id = RelativeHumidity.AttributeDefs.measured_value.id + zcl_pressure_id = PressureMeasurement.AttributeDefs.measured_value.id + zcl_power_voltage_id = PowerConfiguration.AttributeDefs.battery_voltage.id + zcl_power_percent_id = ( + PowerConfiguration.AttributeDefs.battery_percentage_remaining.id + ) + + device.handle_message( + 260, + basic_cluster.cluster_id, + basic_cluster.endpoint.endpoint_id, + basic_cluster.endpoint.endpoint_id, + raw_report, + ) + + assert len(temperature_listener.attribute_updates) == 1 + assert temperature_listener.attribute_updates[0][0] == zcl_temperature_id + assert temperature_listener.attribute_updates[0][1] == expected_results[0] + + assert len(humidity_listener.attribute_updates) == 1 + assert humidity_listener.attribute_updates[0][0] == zcl_humidity_id + assert humidity_listener.attribute_updates[0][1] == expected_results[1] + + assert len(pressure_listener.attribute_updates) == 1 + assert pressure_listener.attribute_updates[0][0] == zcl_pressure_id + assert pressure_listener.attribute_updates[0][1] == expected_results[2] + + assert len(power_listener.attribute_updates) == 2 + assert power_listener.attribute_updates[0][0] == zcl_power_voltage_id + assert power_listener.attribute_updates[0][1] == expected_results[3] + assert power_listener.attribute_updates[1][0] == zcl_power_percent_id + assert power_listener.attribute_updates[1][1] == expected_results[4] + + +@pytest.mark.parametrize( + "raw_report, expected_results", + ( + [ + "1C5F11C10A01FF41210121DB0B03281F0421A8430521B60006240B000000000A21CA356410000B210800", + [ + 3100, # temperature + 9031.899869919436, # illuminance + 30.4, # battery voltage + 154, # battery percent * 2 + ], + ], + ), +) +async def test_xiaomi_motion_sensor_misc( + zigpy_device_from_quirk, raw_report, expected_results +): + """Test device temperature, illuminance, and power from old Aqara motion sensor models.""" + raw_report = bytes.fromhex(raw_report) + + device = zigpy_device_from_quirk(zhaquirks.xiaomi.aqara.motion_aq2.MotionAQ2) + + basic_cluster = device.endpoints[1].basic + + device_temperature_cluster = device.endpoints[1].device_temperature + device_temperature_listener = ClusterListener(device_temperature_cluster) + + illuminance_cluster = device.endpoints[1].illuminance + illuminance_listener = ClusterListener(illuminance_cluster) + + power_cluster = device.endpoints[1].power + power_listener = ClusterListener(power_cluster) + + zcl_device_temperature_id = DeviceTemperature.AttributeDefs.current_temperature.id + zcl_illuminance_id = IlluminanceMeasurement.AttributeDefs.measured_value.id + zcl_power_voltage_id = PowerConfiguration.AttributeDefs.battery_voltage.id + zcl_power_percent_id = ( + PowerConfiguration.AttributeDefs.battery_percentage_remaining.id + ) + + device.handle_message( + 260, + basic_cluster.cluster_id, + basic_cluster.endpoint.endpoint_id, + basic_cluster.endpoint.endpoint_id, + raw_report, + ) + + assert len(device_temperature_listener.attribute_updates) == 1 + assert ( + device_temperature_listener.attribute_updates[0][0] == zcl_device_temperature_id + ) + assert device_temperature_listener.attribute_updates[0][1] == expected_results[0] + + assert len(illuminance_listener.attribute_updates) == 1 + assert illuminance_listener.attribute_updates[0][0] == zcl_illuminance_id + assert illuminance_listener.attribute_updates[0][1] == expected_results[1] + + assert len(power_listener.attribute_updates) == 2 + assert power_listener.attribute_updates[0][0] == zcl_power_voltage_id + assert power_listener.attribute_updates[0][1] == expected_results[2] + assert power_listener.attribute_updates[1][0] == zcl_power_percent_id + assert power_listener.attribute_updates[1][1] == expected_results[3] + + +@pytest.mark.parametrize("quirk", (zhaquirks.xiaomi.aqara.plug.Plug,)) +async def test_xiaomi_power_cluster_not_used(zigpy_device_from_quirk, caplog, quirk): + """Test that a log is printed which warns when a device reports battery mV readout, + even though XiaomiPowerConfigurationCluster is not used. + + This explicitly uses the Plug quirk which will always report this message, as this shouldn't have a battery readout. + Other battery-powered devices might implement the XiaomiPowerConfigurationCluster in the future, + so they would no longer report this message. + """ + caplog.set_level(logging.DEBUG) # relevant message is currently DEBUG level + + device = zigpy_device_from_quirk(quirk) + basic_cluster = device.endpoints[1].basic + + power_cluster = device.endpoints[1].power + power_listener = ClusterListener(power_cluster) + + # fake a battery voltage attribute report + basic_cluster.update_attribute( + XIAOMI_AQARA_ATTRIBUTE, create_aqara_attr_report({1: 2300}) + ) + + # confirm that no battery voltage attribute was updated + assert len(power_listener.attribute_updates) == 0 + + # confirm that a debug message was logged + assert ( + "Xiaomi battery voltage attribute received but XiaomiPowerConfiguration not used" + in caplog.text + ) + + +@pytest.mark.parametrize( + "quirk", (zhaquirks.xiaomi.aqara.roller_curtain_e1.RollerE1AQ,) +) +async def test_xiaomi_e1_roller_curtain_battery(zigpy_device_from_quirk, quirk): + """Test Aqara E1 roller curtain battery reporting.""" + # Ideally, get a real Xiaomi "heartbeat" message to test. + # For now, fake the heartbeat message and check if battery parsing works. + + device = zigpy_device_from_quirk(quirk) + + basic_cluster = device.endpoints[1].basic + ClusterListener(basic_cluster) + + power_cluster = device.endpoints[1].power + power_listener = ClusterListener(power_cluster) + + zcl_power_voltage_id = PowerConfiguration.AttributeDefs.battery_voltage.id + zcl_power_percent_id = ( + PowerConfiguration.AttributeDefs.battery_percentage_remaining.id + ) + + # battery voltage: 2895 mV + # battery percentage: 80% + basic_cluster.update_attribute( + XIAOMI_AQARA_ATTRIBUTE, create_aqara_attr_report({1: 2895, 101: 60}) + ) + + # confirm that battery voltage attribute and percentage were each updated just once, + # so we verify the percent value sent was used, + # and the voltage value sent was only used for the voltage and not also for the percentage + assert len(power_listener.attribute_updates) == 2 + + # verify voltage and percentage values match the values sent + assert power_listener.attribute_updates[0][0] == zcl_power_voltage_id + assert power_listener.attribute_updates[0][1] == 28.9 + assert power_listener.attribute_updates[1][0] == zcl_power_percent_id + assert power_listener.attribute_updates[1][1] == 120 diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index b4ebdb0ed0..8f2be59d4a 100644 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -3,9 +3,11 @@ import asyncio import importlib +import importlib.util import logging import pathlib import pkgutil +import sys from typing import Any import zigpy.device @@ -449,9 +451,13 @@ def setup(custom_quirks_path: str | None = None) -> None: # Treat the custom quirk path (e.g. `/config/custom_quirks/`) itself as a module for importer, modname, _ispkg in pkgutil.walk_packages(path=[str(path)]): + _LOGGER.debug("Loading custom quirk module %r", modname) + try: - _LOGGER.debug("Loading custom quirk module %r", modname) - importer.find_module(modname).load_module(modname) + spec = importer.find_spec(modname) + module = importlib.util.module_from_spec(spec) + sys.modules[modname] = module + spec.loader.exec_module(module) except Exception: _LOGGER.exception("Unexpected exception importing custom quirk %r", modname) else: diff --git a/zhaquirks/aurora/aurora_dimmer.py b/zhaquirks/aurora/aurora_dimmer.py index 470c9f55e7..17a5fcb6a5 100644 --- a/zhaquirks/aurora/aurora_dimmer.py +++ b/zhaquirks/aurora/aurora_dimmer.py @@ -1,5 +1,5 @@ """Device handler for Aurora dimmer switch, battery powered.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -118,8 +118,8 @@ class WallSwitchColorCluster(EventableCluster, Color): # device_version=0 # input_clusters=[] # output_clusters=[33] - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -165,8 +165,8 @@ class WallSwitchColorCluster(EventableCluster, Color): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/develco/power_plug.py b/zhaquirks/develco/power_plug.py new file mode 100644 index 0000000000..6fddb78094 --- /dev/null +++ b/zhaquirks/develco/power_plug.py @@ -0,0 +1,106 @@ +"""Develco smart plugs.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.general import ( + Alarms, + Basic, + DeviceTemperature, + Groups, + Identify, + OnOff, + Ota, + Scenes, + Time, +) +from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement +from zigpy.zcl.clusters.measurement import OccupancySensing +from zigpy.zcl.clusters.smartenergy import Metering + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.develco import DEVELCO + +DEV_TEMP_ID = DeviceTemperature.attributes_by_name["current_temperature"].id + + +class DevelcoDeviceTemperature(CustomCluster, DeviceTemperature): + """Custom device temperature cluster to multiply the temperature by 100.""" + + def _update_attribute(self, attrid, value): + if attrid == DEV_TEMP_ID: + value = value * 100 + super()._update_attribute(attrid, value) + + +class SPLZB131(CustomDevice): + """Custom device Develco smart plug device.""" + + signature = { + MODELS_INFO: [(DEVELCO, "SPLZB-131")], + ENDPOINTS: { + 1: { + PROFILE_ID: 0xC0C9, + DEVICE_TYPE: 1, + INPUT_CLUSTERS: [ + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Alarms.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Time.cluster_id, + Ota.cluster_id, + OccupancySensing.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DevelcoDeviceTemperature, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Alarms.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Time.cluster_id, + Ota.cluster_id, + OccupancySensing.cluster_id, + ], + }, + } + } diff --git a/zhaquirks/gledopto/glc009p.py b/zhaquirks/gledopto/glc009p.py index f50b14baef..2a8e21af59 100644 --- a/zhaquirks/gledopto/glc009p.py +++ b/zhaquirks/gledopto/glc009p.py @@ -1,5 +1,5 @@ """GLEDOPTO GL-C-009P device.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -48,8 +48,8 @@ class GLC009P(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, @@ -77,8 +77,8 @@ class GLC009P(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, diff --git a/zhaquirks/heiman/smoke.py b/zhaquirks/heiman/smoke.py index c391926e12..162a5f64fd 100644 --- a/zhaquirks/heiman/smoke.py +++ b/zhaquirks/heiman/smoke.py @@ -2,7 +2,14 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import Alarms, Basic, Identify, Ota, PowerConfiguration +from zigpy.zcl.clusters.general import ( + Alarms, + Basic, + Identify, + Ota, + PollControl, + PowerConfiguration, +) from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.security import IasWd, IasZone import zigpy.zdo.types @@ -243,3 +250,61 @@ class HeimanSmokeN30(CustomDevice): }, }, } + + +class HeimanSmokeEF30(CustomDevice): + """SmokeEF30 quirk.""" + + # NodeDescriptor( + # logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, + # frequency_band=, mac_capability_flags=, + # manufacturer_code=4619, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, + # descriptor_capability_field=, + # *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, + # *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False) + signature = { + MODELS_INFO: [("HEIMAN", "SmokeSensor-EF-3.0")], + ENDPOINTS: { + # "profile_id": "0x0104", "device_type": "0x0402", + # "input_clusters": ["0x0000", "0x0001", "0x0003", "0x0020", "0x0500", "0x0502", "0x0b05"], + # "output_clusters": ["0x0003", "0x0019"] + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + PollControl.cluster_id, + IasZone.cluster_id, + IasWd.cluster_id, + Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Ota.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + PollControl.cluster_id, + IasZone.cluster_id, + Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Ota.cluster_id, + ], + }, + }, + } diff --git a/zhaquirks/ikea/starkvind.py b/zhaquirks/ikea/starkvind.py index 88d6a57171..9e3c1bf591 100644 --- a/zhaquirks/ikea/starkvind.py +++ b/zhaquirks/ikea/starkvind.py @@ -4,7 +4,7 @@ import logging from typing import Any -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.zcl.clusters.general import ( @@ -121,7 +121,7 @@ async def read_attributes( await self.endpoint.device.endpoints[1] .in_clusters[64637] .read_attributes( - {"air_quality_25pm"}, + ["air_quality_25pm"], allow_cache=allow_cache, only_cache=only_cache, manufacturer=manufacturer, @@ -177,8 +177,8 @@ def __init__(self, *args, **kwargs): # device_version=0 # input_clusters=[33] output_clusters=[33]> 242: { - PROFILE_ID: 0xA1E0, # 41440 (dec) - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, # 41440 (dec) + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, # 0x0021 = GreenPowerProxy.cluster_id @@ -210,8 +210,8 @@ def __init__(self, *args, **kwargs): # device_version=0 # input_clusters=[33] output_clusters=[33]> 242: { - PROFILE_ID: 0xA1E0, # 41440 (dec) - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, # 41440 (dec) + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, # 0x0021 = GreenPowerProxy.cluster_id @@ -253,8 +253,8 @@ class IkeaSTARKVIND_v2(IkeaSTARKVIND): # device_version=0 # input_clusters=[33] output_clusters=[33]> 242: { - PROFILE_ID: 0xA1E0, # 41440 (dec) - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, # 41440 (dec) + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, # 0x0021 = GreenPowerProxy.cluster_id @@ -287,8 +287,8 @@ class IkeaSTARKVIND_v2(IkeaSTARKVIND): # device_version=0 # input_clusters=[33] output_clusters=[33]> 242: { - PROFILE_ID: 0xA1E0, # 41440 (dec) - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, # 41440 (dec) + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, # 0x0021 = GreenPowerProxy.cluster_id diff --git a/zhaquirks/ikea/tradfriplug.py b/zhaquirks/ikea/tradfriplug.py index aa4dfe597e..963bbff51a 100644 --- a/zhaquirks/ikea/tradfriplug.py +++ b/zhaquirks/ikea/tradfriplug.py @@ -1,8 +1,9 @@ """Tradfri Plug Quirk.""" -from zigpy.profiles import zha, zll +from zigpy.profiles import zgp, zha, zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, + GreenPowerProxy, Groups, Identify, LevelControl, @@ -24,7 +25,7 @@ from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID -class TradfriPlug(CustomDevice): +class TradfriPlug1(CustomDevice): """Tradfri Plug.""" signature = { @@ -64,10 +65,10 @@ class TradfriPlug(CustomDevice): # device_version=0 # input_clusters=[33] output_clusters=[33]> 242: { - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0061, - INPUT_CLUSTERS: [33], - OUTPUT_CLUSTERS: [33], + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -93,3 +94,73 @@ class TradfriPlug(CustomDevice): } } } + + +class TradfriPlug2(CustomDevice): + """Tradfri Plug.""" + + signature = { + MODELS_INFO: [(IKEA, "TRADFRI control outlet")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Scenes.cluster_id, + Ota.cluster_id, + PollControl.cluster_id, + LightLink.cluster_id, + ], + }, + # + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LightLink.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Scenes.cluster_id, + Ota.cluster_id, + PollControl.cluster_id, + LightLink.cluster_id, + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } diff --git a/zhaquirks/iluminize/cct.py b/zhaquirks/iluminize/cct.py index 5950d1f520..3b0c51ae18 100644 --- a/zhaquirks/iluminize/cct.py +++ b/zhaquirks/iluminize/cct.py @@ -1,5 +1,5 @@ """Quirk for iluminize CCT actor.""" -from zigpy.profiles import zll +from zigpy.profiles import zgp, zll from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -64,8 +64,8 @@ class CCTLight(CustomDevice): # device_version=0 # input_clusters=[33] # output_clusters=[33] - PROFILE_ID: 41440, - DEVICE_TYPE: 102, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -91,8 +91,8 @@ class CCTLight(CustomDevice): OUTPUT_CLUSTERS: [Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 102, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/iluminize/dim.py b/zhaquirks/iluminize/dim.py index c718cf98b7..69f4a24bd1 100644 --- a/zhaquirks/iluminize/dim.py +++ b/zhaquirks/iluminize/dim.py @@ -1,5 +1,5 @@ """Quirk for iluminize DIM actor.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -64,8 +64,8 @@ class DIMLight(CustomDevice): # device_version=0 # input_clusters=[33] # output_clusters=[33] - PROFILE_ID: 41440, - DEVICE_TYPE: 102, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -91,8 +91,8 @@ class DIMLight(CustomDevice): OUTPUT_CLUSTERS: [Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 102, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/innr/innr_sp234_plug.py b/zhaquirks/innr/innr_sp234_plug.py index 6e24f41d59..f0e86f889c 100644 --- a/zhaquirks/innr/innr_sp234_plug.py +++ b/zhaquirks/innr/innr_sp234_plug.py @@ -1,5 +1,5 @@ """Innr SP 234 plug.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -64,8 +64,8 @@ class SP234(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -98,8 +98,8 @@ class SP234(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/innr/rs228t.py b/zhaquirks/innr/rs228t.py index a4374cd7e7..32f789dea7 100644 --- a/zhaquirks/innr/rs228t.py +++ b/zhaquirks/innr/rs228t.py @@ -1,5 +1,5 @@ """Innr RS 228 T device.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.profiles.zha import DeviceType from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( @@ -55,8 +55,8 @@ class RS228T(CustomDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -81,8 +81,8 @@ class RS228T(CustomDevice): OUTPUT_CLUSTERS: [Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/inovelli/VZM31SN.py b/zhaquirks/inovelli/VZM31SN.py index 02173951e6..829f84ee08 100644 --- a/zhaquirks/inovelli/VZM31SN.py +++ b/zhaquirks/inovelli/VZM31SN.py @@ -1,10 +1,11 @@ """VZM31-SN Two in One Switch/Dimmer Module.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.profiles.zha import DeviceType from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, + GreenPowerProxy, Groups, Identify, LevelControl, @@ -70,10 +71,10 @@ class InovelliVZM31SNv12(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -112,10 +113,10 @@ class InovelliVZM31SNv12(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -159,10 +160,10 @@ class InovelliVZM31SNv11(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -205,10 +206,10 @@ class InovelliVZM31SNv11(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } diff --git a/zhaquirks/inovelli/VZM35SN.py b/zhaquirks/inovelli/VZM35SN.py new file mode 100644 index 0000000000..911fc64671 --- /dev/null +++ b/zhaquirks/inovelli/VZM35SN.py @@ -0,0 +1,124 @@ +"""VZM35-SN Fan Switch.""" + +from zigpy.profiles import zgp, zha +from zigpy.profiles.zha import DeviceType +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + GreenPowerProxy, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.homeautomation import Diagnostic + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.inovelli import INOVELLI_AUTOMATION_TRIGGERS, Inovelli_VZM35SN_Cluster + +INOVELLI_VZM35SN_CLUSTER_ID = 64561 +WWAH_CLUSTER_ID = 64599 + + +class InovelliVZM35SN(CustomDevice): + """VZM35-SN Fan Switch""" + + signature = { + MODELS_INFO: [("Inovelli", "VZM35-SN")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Diagnostic.cluster_id, + INOVELLI_VZM35SN_CLUSTER_ID, + WWAH_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: DeviceType.DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Diagnostic.cluster_id, + Inovelli_VZM35SN_Cluster, + WWAH_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: DeviceType.DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + device_automation_triggers = INOVELLI_AUTOMATION_TRIGGERS diff --git a/zhaquirks/inovelli/__init__.py b/zhaquirks/inovelli/__init__.py index 84f3cc2ff5..7cee57fb14 100644 --- a/zhaquirks/inovelli/__init__.py +++ b/zhaquirks/inovelli/__init__.py @@ -247,12 +247,44 @@ def handle_cluster_request( return +VZM35SN_REMOVES = [ + 0x0012, + 0x0013, + 0x0014, + 0x0019, + 0x0034, + 0x0064, + 0x007D, + 0x0105, +] + + +class Inovelli_VZM35SN_Cluster(Inovelli_VZM31SN_Cluster): + """Inovelli VZM35-SN custom cluster.""" + + attributes = { + key: Inovelli_VZM31SN_Cluster.attributes[key] + for key in Inovelli_VZM31SN_Cluster.attributes + if key not in VZM35SN_REMOVES + } + attributes.update( + { + 0x0017: ("quick_start_time", t.uint8_t, True), + 0x001E: ("non_neutral_aux_med_gear_learn_value", t.uint8_t, True), + 0x001F: ("non_neutral_aux_low_gear_learn_value", t.uint8_t, True), + 0x0034: ("smart_fan_mode", t.Bool, True), + 0x0106: ("smart_fan_led_display_levels", t.uint8_t, True), + } + ) + + INOVELLI_AUTOMATION_TRIGGERS = { (COMMAND_PRESS, ON): {COMMAND: f"{BUTTON_2}_{COMMAND_PRESS}"}, (COMMAND_PRESS, OFF): {COMMAND: f"{BUTTON_1}_{COMMAND_PRESS}"}, (COMMAND_PRESS, CONFIG): {COMMAND: f"{BUTTON_3}_{COMMAND_PRESS}"}, (COMMAND_HOLD, ON): {COMMAND: f"{BUTTON_2}_{COMMAND_HOLD}"}, (COMMAND_HOLD, OFF): {COMMAND: f"{BUTTON_1}_{COMMAND_HOLD}"}, + (COMMAND_HOLD, CONFIG): {COMMAND: f"{BUTTON_3}_{COMMAND_HOLD}"}, (DOUBLE_PRESS, ON): {COMMAND: f"{BUTTON_2}_{COMMAND_DOUBLE}"}, (DOUBLE_PRESS, CONFIG): {COMMAND: f"{BUTTON_3}_{COMMAND_DOUBLE}"}, (DOUBLE_PRESS, OFF): {COMMAND: f"{BUTTON_1}_{COMMAND_DOUBLE}"}, @@ -267,4 +299,5 @@ def handle_cluster_request( (QUINTUPLE_PRESS, CONFIG): {COMMAND: f"{BUTTON_3}_{COMMAND_QUINTUPLE}"}, (COMMAND_RELEASE, ON): {COMMAND: f"{BUTTON_2}_{COMMAND_RELEASE}"}, (COMMAND_RELEASE, OFF): {COMMAND: f"{BUTTON_1}_{COMMAND_RELEASE}"}, + (COMMAND_RELEASE, CONFIG): {COMMAND: f"{BUTTON_3}_{COMMAND_RELEASE}"}, } diff --git a/zhaquirks/insta/nexentro_pushbutton_interface.py b/zhaquirks/insta/nexentro_pushbutton_interface.py index 0c1badfaca..abb9b5c74e 100644 --- a/zhaquirks/insta/nexentro_pushbutton_interface.py +++ b/zhaquirks/insta/nexentro_pushbutton_interface.py @@ -1,5 +1,5 @@ """Device handler for Insta NEXENTRO Pushbutton Interface.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.general import ( @@ -121,8 +121,8 @@ class InstaNexentroPushbuttonInterface(CustomDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -180,8 +180,8 @@ class InstaNexentroPushbuttonInterface(CustomDevice): ], }, 242: { - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/legrand/__init__.py b/zhaquirks/legrand/__init__.py index c5c474299e..41d5f8d55a 100644 --- a/zhaquirks/legrand/__init__.py +++ b/zhaquirks/legrand/__init__.py @@ -1,2 +1,30 @@ """Module for Legrand devices.""" + +from zigpy.quirks import CustomCluster +import zigpy.types as t +from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster + +from zhaquirks import PowerConfigurationCluster + LEGRAND = "Legrand" +MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFC01 # decimal = 64513 + + +class LegrandCluster(CustomCluster, ManufacturerSpecificCluster): + """LegrandCluster.""" + + cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID + name = "LegrandCluster" + ep_attribute = "legrand_cluster" + attributes = { + 0x0000: ("dimmer", t.data16, True), + 0x0001: ("led_dark", t.Bool, True), + 0x0002: ("led_on", t.Bool, True), + } + + +class LegrandPowerConfigurationCluster(PowerConfigurationCluster): + """PowerConfiguration conversor 'V --> %' for Legrand devices.""" + + MIN_VOLTS = 2.5 + MAX_VOLTS = 3.0 diff --git a/zhaquirks/legrand/dimmer.py b/zhaquirks/legrand/dimmer.py index 46abb40c69..c063365d2f 100644 --- a/zhaquirks/legrand/dimmer.py +++ b/zhaquirks/legrand/dimmer.py @@ -1,7 +1,7 @@ -"""Device handler for Legrand Dimmer switch w/o neutral.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomCluster, CustomDevice -import zigpy.types as t +"""Module for Legrand dimmers.""" + +from zigpy.profiles import zgp, zha +from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, BinaryInput, @@ -15,7 +15,6 @@ Scenes, ) from zigpy.zcl.clusters.lighting import Ballast -from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster from zhaquirks import PowerConfigurationCluster from zhaquirks.const import ( @@ -26,29 +25,12 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.legrand import LEGRAND - -MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFC01 # decimal = 64513 - - -class LegrandCluster(CustomCluster, ManufacturerSpecificCluster): - """LegrandCluster.""" - - cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID - name = "LegrandCluster" - ep_attribute = "legrand_cluster" - attributes = { - 0x0000: ("dimmer", t.data16, True), - 0x0001: ("led_dark", t.Bool, True), - 0x0002: ("led_on", t.Bool, True), - } - - -class LegrandPowerConfigurationCluster(PowerConfigurationCluster): - """PowerConfiguration conversor 'V --> %' for Legrand devices.""" - - MIN_VOLTS = 2.5 - MAX_VOLTS = 3.0 +from zhaquirks.legrand import ( + LEGRAND, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + LegrandCluster, + LegrandPowerConfigurationCluster, +) class DimmerWithoutNeutral(CustomDevice): @@ -134,10 +116,10 @@ class DimmerWithoutNeutral2(DimmerWithoutNeutral): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -175,10 +157,10 @@ class DimmerWithoutNeutral3(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0066, - INPUT_CLUSTERS: [0x0021], - OUTPUT_CLUSTERS: [0x0021], + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, + INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -211,8 +193,8 @@ class DimmerWithoutNeutral3(CustomDevice): }, # Green Power End Point 242: { - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0066, # GP Combo Minimum + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -253,10 +235,10 @@ class DimmerWithoutNeutralAndBallast(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0066, - INPUT_CLUSTERS: [0x0021], - OUTPUT_CLUSTERS: [0x0021], + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, + INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -287,8 +269,8 @@ class DimmerWithoutNeutralAndBallast(CustomDevice): }, # Green Power End Point 242: { - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0066, # GP Combo Minimum + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -326,10 +308,10 @@ class DimmerWithNeutral(DimmerWithoutNeutral): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0066, - INPUT_CLUSTERS: [0x0021], - OUTPUT_CLUSTERS: [0x0021], + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, + INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -366,10 +348,10 @@ class DimmerWithNeutral2(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0066, - INPUT_CLUSTERS: [0x0021], - OUTPUT_CLUSTERS: [0x0021], + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, + INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } diff --git a/zhaquirks/legrand/switch.py b/zhaquirks/legrand/switch.py new file mode 100644 index 0000000000..dd8fbb85eb --- /dev/null +++ b/zhaquirks/legrand/switch.py @@ -0,0 +1,77 @@ +"""Module for Legrand switches (without dimming functionality).""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + BinaryInput, + Groups, + Identify, + OnOff, + Ota, + Scenes, +) + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.legrand import LEGRAND, MANUFACTURER_SPECIFIC_CLUSTER_ID, LegrandCluster + + +class LightSwitchWithNeutral(CustomDevice): + """Light switch with neutral wire.""" + + signature = { + # + MODELS_INFO: [(f" {LEGRAND}", " Light switch with neutral")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + BinaryInput.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Ota.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + BinaryInput.cluster_id, + LegrandCluster, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Ota.cluster_id, + LegrandCluster, + ], + } + } + } diff --git a/zhaquirks/lidl/TS0501A.py b/zhaquirks/lidl/TS0501A.py index 64b9e4e718..0803440aef 100644 --- a/zhaquirks/lidl/TS0501A.py +++ b/zhaquirks/lidl/TS0501A.py @@ -1,5 +1,5 @@ """Lidl dimmable bulb.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -57,8 +57,8 @@ class DimmableBulb(CustomDevice): # 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/lixee/zlinky.py b/zhaquirks/lixee/zlinky.py index a2e29e49dc..7695aae031 100644 --- a/zhaquirks/lixee/zlinky.py +++ b/zhaquirks/lixee/zlinky.py @@ -1,7 +1,7 @@ """Quirk for ZLinky_TIC.""" from copy import deepcopy -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.zcl.clusters.general import ( @@ -166,8 +166,8 @@ class ZLinkyTIC(CustomDevice): OUTPUT_CLUSTERS: [Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -190,8 +190,8 @@ class ZLinkyTIC(CustomDevice): OUTPUT_CLUSTERS: [Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/mli/tintE14rgbcct.py b/zhaquirks/mli/tintE14rgbcct.py index 875781109e..b34336dcb2 100644 --- a/zhaquirks/mli/tintE14rgbcct.py +++ b/zhaquirks/mli/tintE14rgbcct.py @@ -1,5 +1,5 @@ """Tint E14 RGB CCT.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -61,8 +61,8 @@ class TintRGBCCTLight(CustomDevice): OUTPUT_CLUSTERS: [Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -88,8 +88,8 @@ class TintRGBCCTLight(CustomDevice): OUTPUT_CLUSTERS: [Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/nodon/switch.py b/zhaquirks/nodon/switch.py index 4bdc2cc1fb..dbb1684920 100644 --- a/zhaquirks/nodon/switch.py +++ b/zhaquirks/nodon/switch.py @@ -1,5 +1,5 @@ """NodOn on/off switch two channels.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -79,8 +79,8 @@ class NodOnSIN4220(CustomDevice): # input_clusters=[33] # output_clusters=[33] 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 102, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -124,8 +124,8 @@ class NodOnSIN4220(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 102, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/siglis/zigfred.py b/zhaquirks/siglis/zigfred.py index 8f99147dce..89475965ee 100644 --- a/zhaquirks/siglis/zigfred.py +++ b/zhaquirks/siglis/zigfred.py @@ -2,7 +2,7 @@ import logging from typing import Any, List, Optional, Union -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.zcl import foundation @@ -187,8 +187,8 @@ def __init__(self, *args, **kwargs): # device_version=0, # input_clusters=[], # output_clusters=[33]) - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -236,8 +236,8 @@ def __init__(self, *args, **kwargs): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -414,8 +414,8 @@ def __init__(self, *args, **kwargs): # device_version=0, # input_clusters=[], # output_clusters=[33]) - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -510,8 +510,8 @@ def __init__(self, *args, **kwargs): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/sinope/sensor.py b/zhaquirks/sinope/sensor.py index 2048d9aecc..8e4d56c1d4 100644 --- a/zhaquirks/sinope/sensor.py +++ b/zhaquirks/sinope/sensor.py @@ -1,12 +1,14 @@ -"""Module to handle quirks of the Sinopé Technologies water leak sensor WL4200 and WL4200S. +"""Module to handle quirks of the Sinopé Technologies water leak sensor and level monitor. It add manufacturer attributes for IasZone cluster for the water leak alarm. +Supported devices are WL4200, WL4200S and LM4110-ZB """ import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.zcl.clusters.general import ( + AnalogInput, Basic, Identify, Ota, @@ -37,7 +39,9 @@ class SinopeManufacturerCluster(CustomCluster): name = "Sinopé Manufacturer specific" ep_attribute = "sinope_manufacturer_specific" attributes = { + 0x0003: ("firmware_number", t.uint16_t, True), 0x0004: ("firmware_version", t.CharacterString, True), + 0x0200: ("unknown_attr_1", t.bitmap32, True), 0xFFFD: ("cluster_revision", t.uint16_t, True), } @@ -45,8 +49,8 @@ class SinopeManufacturerCluster(CustomCluster): class SinopeTechnologiesIasZoneCluster(CustomCluster, IasZone): """SinopeTechnologiesIasZoneCluster custom cluster.""" - class ZoneStatus(t.enum8): - """zone_status values.""" + class LeakStatus(t.enum8): + """leak_status values.""" Dry = 0x00 Leak = 0x01 @@ -54,7 +58,7 @@ class ZoneStatus(t.enum8): attributes = IasZone.attributes.copy() attributes.update( { - 0x0030: ("zone_status", ZoneStatus, True), + 0x0030: ("leak_status", LeakStatus, True), } ) @@ -169,3 +173,57 @@ class SinopeTechnologiesSensor2(CustomDevice): } } } + + +class SinopeTechnologiesLevelMonitor(CustomDevice): + """SinopeTechnologiesLevelMonitor custom device.""" + + signature = { + # + MODELS_INFO: [ + (SINOPE, "LM4110-ZB"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + AnalogInput.cluster_id, + PollControl.cluster_id, + TemperatureMeasurement.cluster_id, + Diagnostic.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + AnalogInput.cluster_id, + PollControl.cluster_id, + TemperatureMeasurement.cluster_id, + Diagnostic.cluster_id, + SinopeManufacturerCluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + } + } diff --git a/zhaquirks/smartwings/wm25lz.py b/zhaquirks/smartwings/wm25lz.py index a394e92ddb..b761a19f54 100644 --- a/zhaquirks/smartwings/wm25lz.py +++ b/zhaquirks/smartwings/wm25lz.py @@ -40,7 +40,6 @@ async def command( *args, manufacturer: int | t.uint16_t | None = None, expect_reply: bool = True, - tries: int = 1, tsn: int | t.uint8_t | None = None, **kwargs: Any, ) -> Coroutine: @@ -56,7 +55,6 @@ async def command( *args, manufacturer=manufacturer, expect_reply=expect_reply, - tries=tries, tsn=tsn, **kwargs, ) diff --git a/zhaquirks/terncy/cl001.py b/zhaquirks/terncy/cl001.py new file mode 100644 index 0000000000..09ebec3ca7 --- /dev/null +++ b/zhaquirks/terncy/cl001.py @@ -0,0 +1,89 @@ +"""Quirk for Xiaoyan CL001 ceiling light.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class ColorClusterTerncy(CustomCluster, Color): + """Set actual supported CCT range and remove RGB color picker since hardware does not support it.""" + + _CONSTANT_ATTRIBUTES = { + Color.AttributeDefs.color_capabilities.id: Color.ColorCapabilities.Color_temperature, + Color.AttributeDefs.color_temp_physical_min.id: 50, + Color.AttributeDefs.color_temp_physical_max.id: 500, + } + + +class TerncyLightCCT(CustomDevice): + """Terncy Light CCT device.""" + + signature = { + MODELS_INFO: [("Xiaoyan", "CL001")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_TEMPERATURE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + 0xFCCC, + 0xFCCD, + 0xFCCE, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_TEMPERATURE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + ColorClusterTerncy, + LightLink.cluster_id, + 0xFCCC, + 0xFCCD, + 0xFCCE, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } diff --git a/zhaquirks/texasinstruments/router.py b/zhaquirks/texasinstruments/router.py index ea7e39532d..fd65bacb0f 100644 --- a/zhaquirks/texasinstruments/router.py +++ b/zhaquirks/texasinstruments/router.py @@ -1,5 +1,5 @@ """Texas Instruments Z-Stack router device.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Identify @@ -39,8 +39,8 @@ class TiRouter(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -60,8 +60,8 @@ class TiRouter(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/thirdreality/night_light.py b/zhaquirks/thirdreality/night_light.py new file mode 100644 index 0000000000..990b2b18e3 --- /dev/null +++ b/zhaquirks/thirdreality/night_light.py @@ -0,0 +1,106 @@ +"""Third Reality night light zigbee devices.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + MultistateInput, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink +from zigpy.zcl.clusters.measurement import IlluminanceMeasurement +from zigpy.zcl.clusters.security import IasZone + +from zhaquirks import LocalDataCluster +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.thirdreality import THIRD_REALITY + +THIRD_REALITY_CLUSTER_ID = 0xFC00 +THIRD_REALITY_MOTION_EVENT_ATTR_ID = 0x0002 + + +class ThirdRealitySpecificCluster(CustomCluster): + """Manufacturer specific cluster to relay motion event to IAS Zone cluster.""" + + cluster_id = THIRD_REALITY_CLUSTER_ID + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == THIRD_REALITY_MOTION_EVENT_ATTR_ID: + self.endpoint.ias_zone.update_attribute( + IasZone.AttributeDefs.zone_status.id, value + ) + + +class LocalIasZone(LocalDataCluster, IasZone): + """Local IAS Zone cluster.""" + + _CONSTANT_ATTRIBUTES = { + IasZone.AttributeDefs.zone_type.id: IasZone.ZoneType.Motion_Sensor + } + + +class Nightlight(CustomDevice): + """Custom device for 3RSNL02043Z.""" + + signature = { + MODELS_INFO: [(THIRD_REALITY, "3RSNL02043Z")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + MultistateInput.cluster_id, + Color.cluster_id, + IlluminanceMeasurement.cluster_id, + LightLink.cluster_id, + ThirdRealitySpecificCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + MultistateInput.cluster_id, + Color.cluster_id, + IlluminanceMeasurement.cluster_id, + LightLink.cluster_id, + LocalIasZone, + ThirdRealitySpecificCluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + } + } diff --git a/zhaquirks/thirdreality/vibrate.py b/zhaquirks/thirdreality/vibrate.py new file mode 100644 index 0000000000..8202dba93b --- /dev/null +++ b/zhaquirks/thirdreality/vibrate.py @@ -0,0 +1,70 @@ +"""Third Reality vibrate devices.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, Ota, PowerConfiguration +from zigpy.zcl.clusters.security import IasZone + +from zhaquirks import CustomCluster +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.thirdreality import THIRD_REALITY + +MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFFF1 + + +class ThirdRealityAccelCluster(CustomCluster): + """ThirdReality Acceleration Cluster.""" + + cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID + attributes = { + 0x0001: ("x_axis", t.int16s, True), + 0x0002: ("y_axis", t.int16s, True), + 0x0003: ("z_axis", t.int16s, True), + } + + +class Vibrate(CustomDevice): + """ThirdReality vibrate device.""" + + signature = { + MODELS_INFO: [(THIRD_REALITY, "3RVS01031Z")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + IasZone.cluster_id, + ThirdRealityAccelCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + IasZone.cluster_id, + ThirdRealityAccelCluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + } + }, + } diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index ddc7e0f136..8ade6c6e6d 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -30,6 +30,7 @@ TUYA_CLUSTER_ID = 0xEF00 TUYA_CLUSTER_E000_ID = 0xE000 TUYA_CLUSTER_E001_ID = 0xE001 +TUYA_CLUSTER_1888_ID = 0x1888 # --------------------------------------------------------- # Tuya Cluster Commands # --------------------------------------------------------- @@ -544,7 +545,7 @@ class TuyaEnchantableCluster(CustomCluster): - clusters which would be bound, but that changed their ep_attribute Make sure to add a subclass of TuyaEnchantableCluster to the quirk replacement. Tests will fail if this is not done. - Classes like TuyaOnOff, TuyaZBOnOffAttributeCluster, TuyaSmartRemoteOnOffCluster already inherit from this class. + Classes like TuyaOnOff, TuyaZBOnOffAttributeCluster, TuyaNoBindPowerConfigurationCluster inherit from this class. """ async def bind(self): @@ -1105,9 +1106,9 @@ class TuyaZBElectricalMeasurement(CustomCluster, ElectricalMeasurement): class TuyaZBE000Cluster(CustomCluster): """Tuya manufacturer specific cluster 57344.""" - name = "Tuya Manufacturer Specific" + name = "Tuya Manufacturer Specific 0" cluster_id = TUYA_CLUSTER_E000_ID - ep_attribute = "tuya_is_pita_0" + ep_attribute = "tuya_manufacturer_specific_57344" # Tuya Zigbee Cluster 0xE001 Implementation @@ -1128,6 +1129,15 @@ class TuyaZBExternalSwitchTypeCluster(CustomCluster): attributes = {0xD030: ("external_switch_type", ExternalSwitchType)} +# Tuya Zigbee Cluster 0x1888 Implementation +class TuyaZB1888Cluster(CustomCluster): + """Tuya manufacturer specific cluster 6280.""" + + name = "Tuya Manufacturer Specific 1" + cluster_id = TUYA_CLUSTER_1888_ID + ep_attribute = "tuya_manufacturer_specific_6280" + + # Tuya Window Cover Implementation class TuyaManufacturerWindowCover(TuyaManufCluster): """Manufacturer Specific Cluster for cover device.""" diff --git a/zhaquirks/tuya/air/ts0601_air_quality.py b/zhaquirks/tuya/air/ts0601_air_quality.py index 8642afba87..dafd9b23d7 100644 --- a/zhaquirks/tuya/air/ts0601_air_quality.py +++ b/zhaquirks/tuya/air/ts0601_air_quality.py @@ -1,6 +1,6 @@ """Tuya Air Quality sensor.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time @@ -103,8 +103,8 @@ class TuyaCO2SensorGPP(CustomDevice): # 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -116,8 +116,8 @@ class Switch_1G_Metering(EnchantedDevice): # input_clusters=[] # output_clusters=["0x0021"]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -189,8 +189,8 @@ class Switch_2G_GPP(EnchantedDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -224,8 +224,8 @@ class Switch_2G_GPP(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -276,8 +276,8 @@ class Switch_2G_Metering(EnchantedDevice): # input_clusters=[] # output_clusters=["0x0021"]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -312,8 +312,8 @@ class Switch_2G_Metering(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -367,8 +367,8 @@ class Switch_2G_Var03(EnchantedDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -404,8 +404,8 @@ class Switch_2G_Var03(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -472,8 +472,8 @@ class Switch_3G_GPP(EnchantedDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -518,8 +518,8 @@ class Switch_3G_GPP(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -583,8 +583,8 @@ class Switch_3G_Metering(EnchantedDevice): # input_clusters=[] # output_clusters=["0x0021"]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -629,8 +629,121 @@ class Switch_3G_Metering(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + +class Switch_3G_GPP_Var2(EnchantedDevice): + """Tuya 3 gang switch module.""" + + signature = { + MODEL: "TS0003", + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -712,8 +825,8 @@ class Switch_4G_GPP(EnchantedDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -769,8 +882,8 @@ class Switch_4G_GPP(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -847,8 +960,8 @@ class Switch_4G_Metering(EnchantedDevice): # input_clusters=[] # output_clusters=["0x0021"]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -903,8 +1016,147 @@ class Switch_4G_Metering(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + +class Switch_4G_GPP_Var2(EnchantedDevice): + """Tuya 4 gang switch module.""" + + signature = { + MODEL: "TS0004", + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/tuya/ts001x.py b/zhaquirks/tuya/ts001x.py index d77ac55f5b..7a1bff3194 100644 --- a/zhaquirks/tuya/ts001x.py +++ b/zhaquirks/tuya/ts001x.py @@ -10,7 +10,6 @@ MODEL, OUTPUT_CLUSTERS, PROFILE_ID, - SKIP_CONFIGURATION, ) from zhaquirks.tuya import ( TuyaSwitch, @@ -50,7 +49,6 @@ class TuyaSingleNoNeutralSwitch(EnchantedDevice, TuyaSwitch): } replacement = { - SKIP_CONFIGURATION: True, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, @@ -112,7 +110,6 @@ class TuyaDoubleNoNeutralSwitch(EnchantedDevice, TuyaSwitch): } replacement = { - SKIP_CONFIGURATION: True, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, @@ -198,7 +195,6 @@ class TuyaTripleNoNeutralSwitch(EnchantedDevice, TuyaSwitch): } replacement = { - SKIP_CONFIGURATION: True, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, @@ -264,7 +260,6 @@ class TuyaSingleNoNeutralSwitch_2(EnchantedDevice, TuyaSwitch): }, } replacement = { - SKIP_CONFIGURATION: True, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, @@ -328,7 +323,6 @@ class TuyaDoubleNoNeutralSwitch_2(EnchantedDevice, TuyaSwitch): } replacement = { - SKIP_CONFIGURATION: True, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, @@ -416,7 +410,6 @@ class TuyaTripleNoNeutralSwitch_2(EnchantedDevice, TuyaSwitch): } replacement = { - SKIP_CONFIGURATION: True, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/tuya/ts004f.py b/zhaquirks/tuya/ts004f.py index 39684f12e0..5aff3d1892 100644 --- a/zhaquirks/tuya/ts004f.py +++ b/zhaquirks/tuya/ts004f.py @@ -19,6 +19,7 @@ from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks.const import ( + ALT_SHORT_PRESS, BUTTON, BUTTON_1, BUTTON_2, @@ -60,6 +61,7 @@ from zhaquirks.tuya import ( TuyaNoBindPowerConfigurationCluster, TuyaSmartRemoteOnOffCluster, + TuyaZBExternalSwitchTypeCluster, ) from zhaquirks.tuya.mcu import EnchantedDevice @@ -78,6 +80,7 @@ class TuyaSmartRemote004FROK(EnchantedDevice): ("_TZ3000_ixla93vd", "TS004F"), ("_TZ3000_qja6nq5z", "TS004F"), ("_TZ3000_csflgqj2", "TS004F"), + ("_TZ3000_abrsvsou", "TS004F"), ], ENDPOINTS: { 1: { @@ -319,6 +322,89 @@ class TuyaSmartRemote004FDMS(EnchantedDevice): } +class TuyaSmartRemote004FSK(EnchantedDevice): + """Tuya Smart (Single) Knob device.""" + + signature = { + # "node_descriptor": "NodeDescriptor(byte1=2, byte2=64, mac_capability_flags=128, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=0, *allocate_address=True, *complex_descriptor_available=False, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False, *is_valid=True, *logical_type=, *user_descriptor_available=False)", + # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=260, device_version=1, input_clusters=[0, 1, 3, 4, 6, 4096, 57345], output_clusters=[25, 10, 3, 4, 6, 8, 4096]) + MODELS_INFO: [ + ("_TZ3000_kjfzuycl", "TS004F"), + ("_TZ3000_ja5osu5g", "TS004F"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LightLink.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + Time.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, + INPUT_CLUSTERS: [ + Basic.cluster_id, + TuyaNoBindPowerConfigurationCluster, + Identify.cluster_id, + Groups.cluster_id, # Is needed for adding group then binding is not working. + LightLink.cluster_id, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + Time.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + TuyaSmartRemoteOnOffCluster, + LevelControl.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + ], + }, + }, + } + + device_automation_triggers = { + (SHORT_PRESS, BUTTON): {COMMAND: COMMAND_ON, ENDPOINT_ID: 1, CLUSTER_ID: 6}, + (DOUBLE_PRESS, BUTTON): {COMMAND: COMMAND_OFF, ENDPOINT_ID: 1, CLUSTER_ID: 6}, + (LONG_PRESS, BUTTON): {COMMAND: COMMAND_STEP, ENDPOINT_ID: 1, CLUSTER_ID: 8}, + (LONG_RELEASE, BUTTON): {COMMAND: COMMAND_STOP, ENDPOINT_ID: 1, CLUSTER_ID: 8}, + (ALT_SHORT_PRESS, BUTTON): { + ENDPOINT_ID: 1, + CLUSTER_ID: 8, + PARAMS: { + "transition_time": 1, + "options_mask": None, + "options_override": None, + }, + }, + (SHORT_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: SHORT_PRESS}, + (LONG_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: LONG_PRESS}, + (DOUBLE_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: DOUBLE_PRESS}, + } + + class TuyaSmartRemote004F(EnchantedDevice): """Tuya 4-button New version remote device.""" diff --git a/zhaquirks/tuya/ts011f_plug.py b/zhaquirks/tuya/ts011f_plug.py index ac4558df0d..1f540e167e 100644 --- a/zhaquirks/tuya/ts011f_plug.py +++ b/zhaquirks/tuya/ts011f_plug.py @@ -1,6 +1,6 @@ """TS011F plug.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -13,6 +13,7 @@ Time, ) from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement +from zigpy.zcl.clusters.lightlink import LightLink from zigpy.zcl.clusters.measurement import TemperatureMeasurement from zigpy.zcl.clusters.smartenergy import Metering @@ -27,6 +28,7 @@ ) from zhaquirks.tuya import ( TuyaNewManufCluster, + TuyaZB1888Cluster, TuyaZBE000Cluster, TuyaZBElectricalMeasurement, TuyaZBExternalSwitchTypeCluster, @@ -68,8 +70,8 @@ class Plug(EnchantedDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -124,8 +126,8 @@ class Plug_1AC(CustomDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -146,8 +148,8 @@ class Plug_1AC(CustomDevice): OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -229,8 +231,8 @@ class Plug_2AC_2USB(EnchantedDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -283,8 +285,8 @@ class Plug_2AC_2USB(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -349,8 +351,8 @@ class Plug_3AC_4USB(CustomDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -393,8 +395,8 @@ class Plug_3AC_4USB(CustomDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -499,8 +501,8 @@ class Plug_4AC_2USB(CustomDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -575,8 +577,8 @@ class Plug_4AC_2USB(CustomDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -630,8 +632,8 @@ class Plug_TZ3210_2AC(CustomDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -668,8 +670,8 @@ class Plug_TZ3210_2AC(CustomDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -707,8 +709,8 @@ class Plug_TZ3210_1AC(CustomDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -733,8 +735,8 @@ class Plug_TZ3210_1AC(CustomDevice): OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -831,8 +833,8 @@ class Plug_4AC_2USB_cfnprab5(EnchantedDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -899,8 +901,8 @@ class Plug_4AC_2USB_cfnprab5(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -969,8 +971,8 @@ class Plug_4AC_2USB_Metering(EnchantedDevice): # "device_type": "0x0061", # "in_clusters": [], # "out_clusters": ["0x0021"] - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -1017,8 +1019,8 @@ class Plug_4AC_2USB_Metering(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -1074,6 +1076,77 @@ class Plug_v2(EnchantedDevice): } +class Plug_v3(EnchantedDevice): + """Tuya TS011F plug. One plug is _Tz3000_0Zfrhq4I.""" + + signature = { + MODEL: "TS011F", + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Time.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + LightLink.cluster_id, + TuyaZB1888Cluster.cluster_id, + TuyaZBE000Cluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + Time.cluster_id, + TuyaZBMeteringClusterWithUnit, + TuyaZBElectricalMeasurement, + LightLink.cluster_id, + TuyaZB1888Cluster, + TuyaZBE000Cluster, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, + ], + }, + }, + } + + class Plug_2AC_var03(CustomDevice): """Tuya 2 socket wall outlet with child lock and power-restore state support.""" @@ -1177,8 +1250,8 @@ class Plug_CB_Metering(EnchantedDevice): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -1203,8 +1276,8 @@ class Plug_CB_Metering(EnchantedDevice): OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -1258,8 +1331,8 @@ class Plug_2AC_var05(EnchantedDevice): # "device_type": "0x0061", # "in_clusters": [], # "out_clusters": ["0x0021"] - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -1295,8 +1368,8 @@ class Plug_2AC_var05(EnchantedDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/tuya/ts011f_switch.py b/zhaquirks/tuya/ts011f_switch.py index 96e5c46935..950f08fc2c 100644 --- a/zhaquirks/tuya/ts011f_switch.py +++ b/zhaquirks/tuya/ts011f_switch.py @@ -1,6 +1,6 @@ """Tuya TS011F Switches.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.zcl.clusters.general import ( Basic, GreenPowerProxy, @@ -71,8 +71,8 @@ class Tuya_2G_Switch(TuyaSwitch): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -108,8 +108,8 @@ class Tuya_2G_Switch(TuyaSwitch): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/tuya/ts0121_plug.py b/zhaquirks/tuya/ts0121_plug.py index a1351a6bf4..286fe57075 100644 --- a/zhaquirks/tuya/ts0121_plug.py +++ b/zhaquirks/tuya/ts0121_plug.py @@ -1,7 +1,16 @@ """Tuya TS0121 plug.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import Basic, Groups, Identify, OnOff, Ota, Scenes, Time +from zigpy.zcl.clusters.general import ( + Basic, + GreenPowerProxy, + Groups, + Identify, + OnOff, + Ota, + Scenes, + Time, +) from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.smartenergy import Metering @@ -90,10 +99,10 @@ class TS0121B(CustomDevice): OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -116,10 +125,10 @@ class TS0121B(CustomDevice): OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -151,14 +160,14 @@ class TS0121_Var03(CustomDevice): OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { - # "profile_id": 41440, + # "profile_id": "0xA1E0", # "device_type": "0x0061", # "in_clusters": [], # "out_clusters": ["0x0021"] - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } @@ -178,10 +187,10 @@ class TS0121_Var03(CustomDevice): OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, 242: { - PROFILE_ID: 0xA1E0, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [0x0021], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, } diff --git a/zhaquirks/tuya/ts0501b.py b/zhaquirks/tuya/ts0501b.py index ece26ef329..80db73b632 100644 --- a/zhaquirks/tuya/ts0501b.py +++ b/zhaquirks/tuya/ts0501b.py @@ -1,5 +1,5 @@ """Tuya dimmable led controller.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Basic, @@ -59,8 +59,8 @@ class DimmableLedController(CustomDevice): # MODELS_INFO: [ ("_TZE200_7hfcudw5", "TS0601"), + ("_TZE200_ppuj1vem", "TS0601"), ], ENDPOINTS: { 1: { @@ -414,8 +415,8 @@ class MmwRadarMotionGPP(CustomDevice): # 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -214,8 +214,8 @@ class TuyaTS130FTOGP(CustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -329,8 +329,8 @@ class TuyaTS130GP(CustomDevice): # Iterator[tuple[foundation.Attribute, bytes]]: - """Yield all interpretations of the first attribute in an Xiaomi report.""" + """Yield all interpretations of the first attribute in a Xiaomi report.""" # Peek at the attribute report attr_id, data = t.uint16_t.deserialize(data) @@ -255,63 +244,95 @@ def _update_attribute(self, attrid, value): return _LOGGER.debug( - "%s - Attribute report. attribute_id: [%s] value: [%s]", + "%s - Xiaomi attribute report. attribute_id: [%s] value: [%s]", self.endpoint.device.ieee, attrid, attributes, ) if BATTERY_VOLTAGE_MV in attributes: - self.endpoint.device.battery_bus.listener_event( - BATTERY_REPORTED, attributes[BATTERY_VOLTAGE_MV] - ) + # many Xiaomi devices report this, but not all quirks implement the XiaomiPowerConfiguration cluster, + # so we might error out if the method doesn't exist + if hasattr(self.endpoint.power, "battery_reported") and callable( + self.endpoint.power.battery_reported + ): + self.endpoint.power.battery_reported(attributes[BATTERY_VOLTAGE_MV]) + else: + # log a debug message if the cluster is not implemented + _LOGGER.debug( + "%s - Xiaomi battery voltage attribute received but XiaomiPowerConfiguration not used", + self.endpoint.device.ieee, + ) if TEMPERATURE_MEASUREMENT in attributes: - self.endpoint.device.temperature_bus.listener_event( - TEMPERATURE_REPORTED, attributes[TEMPERATURE_MEASUREMENT] + self.endpoint.temperature.update_attribute( + TemperatureMeasurement.AttributeDefs.measured_value.id, + attributes[TEMPERATURE_MEASUREMENT], ) + if HUMIDITY_MEASUREMENT in attributes: - self.endpoint.device.humidity_bus.listener_event( - HUMIDITY_REPORTED, attributes[HUMIDITY_MEASUREMENT] + self.endpoint.humidity.update_attribute( + RelativeHumidity.AttributeDefs.measured_value.id, + attributes[HUMIDITY_MEASUREMENT], ) + if PRESSURE_MEASUREMENT in attributes: - self.endpoint.device.pressure_bus.listener_event( - PRESSURE_REPORTED, attributes[PRESSURE_MEASUREMENT] / 100 + self.endpoint.pressure.update_attribute( + PressureMeasurement.AttributeDefs.measured_value.id, + attributes[PRESSURE_MEASUREMENT] / 100, ) + if POWER in attributes: - self.endpoint.device.power_bus.listener_event( - POWER_REPORTED, attributes[POWER] + self.endpoint.electrical_measurement.update_attribute( + ElectricalMeasurement.AttributeDefs.active_power.id, + round(attributes[POWER] * 10), ) + if CONSUMPTION in attributes: - self.endpoint.device.consumption_bus.listener_event( - CONSUMPTION_REPORTED, attributes[CONSUMPTION] + zcl_consumption = round(attributes[CONSUMPTION] * 1000) + self.endpoint.electrical_measurement.update_attribute( + ElectricalMeasurement.AttributeDefs.total_active_power.id, + zcl_consumption, + ) + self.endpoint.smartenergy_metering.update_attribute( + Metering.AttributeDefs.current_summ_delivered.id, zcl_consumption ) + if VOLTAGE in attributes: - self.endpoint.device.voltage_bus.listener_event( - VOLTAGE_REPORTED, attributes[VOLTAGE] * 0.1 + self.endpoint.electrical_measurement.update_attribute( + ElectricalMeasurement.AttributeDefs.rms_voltage.id, + attributes[VOLTAGE] * 0.1, ) + if ILLUMINANCE_MEASUREMENT in attributes: - self.endpoint.device.illuminance_bus.listener_event( - ILLUMINANCE_REPORTED, attributes[ILLUMINANCE_MEASUREMENT] + self.endpoint.illuminance.update_attribute( + IlluminanceMeasurement.AttributeDefs.measured_value.id, + attributes[ILLUMINANCE_MEASUREMENT], ) + if TVOC_MEASUREMENT in attributes: self.endpoint.voc_level.update_attribute( 0x0000, attributes[TVOC_MEASUREMENT] ) + if TEMPERATURE in attributes: if hasattr(self.endpoint, "device_temperature"): self.endpoint.device_temperature.update_attribute( - 0x0000, attributes[TEMPERATURE] * 100 + DeviceTemperature.AttributeDefs.current_temperature.id, + attributes[TEMPERATURE] * 100, ) + if BATTERY_PERCENTAGE_REMAINING_ATTRIBUTE in attributes: - self.endpoint.device.power_bus_percentage.listener_event( - "update_battery_percentage", - attributes[BATTERY_PERCENTAGE_REMAINING_ATTRIBUTE], + self.endpoint.power.battery_percent_reported( + attributes[BATTERY_PERCENTAGE_REMAINING_ATTRIBUTE] ) + if SMOKE in attributes: - self.endpoint.ias_zone.update_attribute(ZONE_STATUS, attributes[SMOKE]) + self.endpoint.ias_zone.update_attribute( + IasZone.AttributeDefs.zone_status.id, attributes[SMOKE] + ) def _parse_aqara_attributes(self, value): - """Parse non standard attributes.""" + """Parse non-standard attributes.""" attributes = {} attribute_names = { 1: BATTERY_VOLTAGE_MV, @@ -346,6 +367,8 @@ def _parse_aqara_attributes(self, value): "lumi.plug.maeu01", "lumi.plug.mmeu01", "lumi.relay.c2acn01", + "lumi.switch.n0agl1", + "lumi.switch.n0acn2", ]: attribute_names.update({149: CONSUMPTION, 150: VOLTAGE, 152: POWER}) elif self.endpoint.device.model == "lumi.sensor_motion.aq2": @@ -398,7 +421,7 @@ def _parse_aqara_attributes(self, value): return attributes def _parse_mija_attributes(self, value): - """Parse non standard attributes.""" + """Parse non-standard attributes.""" attribute_names = ( STATE, BATTERY_VOLTAGE_MV, @@ -436,15 +459,16 @@ class BinaryOutputInterlock(CustomCluster, BinaryOutput): class XiaomiPowerConfiguration(PowerConfiguration, LocalDataCluster): """Xiaomi power configuration cluster implementation.""" - BATTERY_VOLTAGE_ATTR = 0x0020 - BATTERY_PERCENTAGE_REMAINING = 0x0021 + BATTERY_VOLTAGE_ATTR = PowerConfiguration.AttributeDefs.battery_voltage.id + BATTERY_PERCENTAGE_REMAINING = ( + PowerConfiguration.AttributeDefs.battery_percentage_remaining.id + ) MAX_VOLTS_MV = 3100 MIN_VOLTS_MV = 2820 def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) - self.endpoint.device.battery_bus.add_listener(self) self._CONSTANT_ATTRIBUTES = { BATTERY_QUANTITY_ATTR: 1, BATTERY_SIZE_ATTR: getattr(self.endpoint.device, BATTERY_SIZE, 0xFF), @@ -466,7 +490,7 @@ def _update_battery_percentage(self, voltage_mv: int) -> None: percent = round((voltage_mv - self.MIN_VOLTS_MV) * self._slope) - self.debug( + _LOGGER.debug( "Voltage mV: [Min]:%s < [RAW]:%s < [Max]:%s, Battery Percent: %s", self.MIN_VOLTS_MV, voltage_mv, @@ -484,7 +508,7 @@ class OccupancyCluster(OccupancyWithReset): class MotionCluster(LocalDataCluster, MotionOnEvent): """Motion cluster.""" - _CONSTANT_ATTRIBUTES = {ZONE_TYPE: MOTION_TYPE} + _CONSTANT_ATTRIBUTES = {IasZone.AttributeDefs.zone_type.id: MOTION_TYPE} reset_s: int = 70 @@ -499,105 +523,70 @@ class XiaomiMeteringCluster(LocalDataCluster, Metering): class TemperatureMeasurementCluster(CustomCluster, TemperatureMeasurement): """Temperature cluster that filters out invalid temperature readings.""" - cluster_id = TemperatureMeasurement.cluster_id - ATTR_ID = 0 - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self.endpoint.device.temperature_bus.add_listener(self) - def _update_attribute(self, attrid, value): # drop values above and below documented range for this sensor # value is in centi degrees - if attrid == self.ATTR_ID and (-6000 <= value <= 6000): + if attrid == self.AttributeDefs.measured_value.id and (-6000 <= value <= 6000): super()._update_attribute(attrid, value) - def temperature_reported(self, value): - """Temperature reported.""" - self._update_attribute(self.ATTR_ID, value) - class RelativeHumidityCluster(CustomCluster, RelativeHumidity): """Humidity cluster that filters out invalid humidity readings.""" - cluster_id = RelativeHumidity.cluster_id - ATTR_ID = 0 - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self.endpoint.device.humidity_bus.add_listener(self) - def _update_attribute(self, attrid, value): # drop values above and below documented range for this sensor - if attrid == self.ATTR_ID and (0 <= value <= 9999): + if attrid == self.AttributeDefs.measured_value.id and (0 <= value <= 9999): super()._update_attribute(attrid, value) - def humidity_reported(self, value): - """Humidity reported.""" - self._update_attribute(self.ATTR_ID, value) - class PressureMeasurementCluster(CustomCluster, PressureMeasurement): """Pressure cluster to receive reports that are sent to the basic cluster.""" - cluster_id = PressureMeasurement.cluster_id - ATTR_ID = 0 - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self.endpoint.device.pressure_bus.add_listener(self) - def _update_attribute(self, attrid, value): # drop unreasonable values # value is in hectopascals - if attrid == self.ATTR_ID and (0 <= value <= 1100): + if attrid == self.AttributeDefs.measured_value.id and (0 <= value <= 1100): super()._update_attribute(attrid, value) - def pressure_reported(self, value): - """Pressure reported.""" - self._update_attribute(self.ATTR_ID, value) - class AnalogInputCluster(CustomCluster, AnalogInput): - """Analog input cluster, only used to relay power consumption information to ElectricalMeasurementCluster.""" + """Analog input cluster, only used to relay power consumption information to ElectricalMeasurementCluster. - cluster_id = AnalogInput.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - self._current_state = {} - super().__init__(*args, **kwargs) + The AnalogInput cluster responsible for reporting power consumption seems to be on endpoint 21 for newer devices + and either on endpoint 1 or 2 for older devices. + """ def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) - if value is not None and value >= 0: - self.endpoint.device.power_bus.listener_event(POWER_REPORTED, value) + if ( + attrid == self.AttributeDefs.present_value.id + and value is not None + and value >= 0 + ): + # ElectricalMeasurementCluster is assumed to be on endpoint 1 + self.endpoint.device.endpoints[1].electrical_measurement.update_attribute( + ElectricalMeasurement.AttributeDefs.active_power.id, + round(value * 10), + ) class ElectricalMeasurementCluster(LocalDataCluster, ElectricalMeasurement): """Electrical measurement cluster to receive reports that are sent to the basic cluster.""" - cluster_id = ElectricalMeasurement.cluster_id - POWER_ID = 0x050B - VOLTAGE_ID = 0x0505 - CONSUMPTION_ID = 0x0304 + POWER_ID = ElectricalMeasurement.AttributeDefs.active_power.id + VOLTAGE_ID = ElectricalMeasurement.AttributeDefs.rms_voltage.id + CONSUMPTION_ID = ElectricalMeasurement.AttributeDefs.total_active_power.id + _CONSTANT_ATTRIBUTES = { - 0x0402: 1, # power_multiplier - 0x0403: 1, # power_divisor - 0x0604: 1, # ac_power_multiplier - 0x0605: 10, # ac_power_divisor + ElectricalMeasurement.AttributeDefs.power_multiplier.id: 1, + ElectricalMeasurement.AttributeDefs.power_divisor.id: 1, + ElectricalMeasurement.AttributeDefs.ac_power_multiplier.id: 1, + ElectricalMeasurement.AttributeDefs.ac_power_divisor.id: 10, } def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) - self.endpoint.device.voltage_bus.add_listener(self) - self.endpoint.device.consumption_bus.add_listener(self) - self.endpoint.device.power_bus.add_listener(self) - # put a default value so the sensors are created if self.POWER_ID not in self._attr_cache: self._update_attribute(self.POWER_ID, 0) @@ -606,66 +595,35 @@ def __init__(self, *args, **kwargs): if self.CONSUMPTION_ID not in self._attr_cache: self._update_attribute(self.CONSUMPTION_ID, 0) - def power_reported(self, value): - """Power reported.""" - self._update_attribute(self.POWER_ID, round(value * 10)) - - def voltage_reported(self, value): - """Voltage reported.""" - self._update_attribute(self.VOLTAGE_ID, value) - - def consumption_reported(self, value): - """Consumption reported.""" - self._update_attribute(self.CONSUMPTION_ID, round(value * 1000)) - class MeteringCluster(LocalDataCluster, Metering): """Metering cluster to receive reports that are sent to the basic cluster.""" - cluster_id = Metering.cluster_id - CURRENT_SUMM_DELIVERED_ID = 0x0000 + CURRENT_SUMM_DELIVERED_ID = Metering.AttributeDefs.current_summ_delivered.id _CONSTANT_ATTRIBUTES = { - 0x0300: 0, # unit_of_measure: kWh - 0x0301: 1, # multiplier - 0x0302: 1000, # divisor - 0x0303: 0b0_0100_011, # summation_formatting (read from plug) - 0x0306: 0, # metering_device_type: electric + Metering.AttributeDefs.unit_of_measure.id: 0, # kWh + Metering.AttributeDefs.multiplier.id: 1, + Metering.AttributeDefs.divisor.id: 1000, + Metering.AttributeDefs.summation_formatting.id: 0b0_0100_011, # read from plug + Metering.AttributeDefs.metering_device_type.id: 0, # electric } def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) - self.endpoint.device.consumption_bus.add_listener(self) - # put a default value so the sensor is created if self.CURRENT_SUMM_DELIVERED_ID not in self._attr_cache: self._update_attribute(self.CURRENT_SUMM_DELIVERED_ID, 0) - def consumption_reported(self, value): - """Consumption reported.""" - self._update_attribute(self.CURRENT_SUMM_DELIVERED_ID, round(value * 1000)) - class IlluminanceMeasurementCluster(CustomCluster, IlluminanceMeasurement): - """Multistate input cluster.""" - - cluster_id = IlluminanceMeasurement.cluster_id - ATTR_ID = 0 - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self.endpoint.device.illuminance_bus.add_listener(self) + """Illuminance measurement cluster.""" def _update_attribute(self, attrid, value): - if attrid == self.ATTR_ID and value > 0: + if attrid == self.AttributeDefs.measured_value.id and value > 0: value = 10000 * math.log10(value) + 1 super()._update_attribute(attrid, value) - def illuminance_reported(self, value): - """Illuminance reported.""" - self._update_attribute(self.ATTR_ID, value) - class OnOffCluster(OnOff, CustomCluster): """Aqara wall switch cluster.""" @@ -676,7 +634,6 @@ def command( *args, manufacturer: int | t.uint16_t | None = None, expect_reply: bool = True, - tries: int = 1, tsn: int | t.uint8_t | None = None, **kwargs: Any, ): diff --git a/zhaquirks/xiaomi/aqara/ctrl_ln.py b/zhaquirks/xiaomi/aqara/ctrl_ln.py index 0d76688646..4be61fafce 100644 --- a/zhaquirks/xiaomi/aqara/ctrl_ln.py +++ b/zhaquirks/xiaomi/aqara/ctrl_ln.py @@ -16,7 +16,7 @@ Time, ) -from zhaquirks import Bus, EventableCluster +from zhaquirks import EventableCluster from zhaquirks.const import ( ARGS, ATTRIBUTE_ID, @@ -71,11 +71,6 @@ class BasicClusterDecoupled(BasicCluster): class WallSwitchMultistateInputCluster(EventableCluster, MultistateInput): """WallSwitchMultistateInputCluster: fire events corresponding to press type.""" - def __init__(self, *args, **kwargs): - """Init.""" - self.power_bus = Bus() - super().__init__(*args, **kwargs) - signature = { MODELS_INFO: [(LUMI, "lumi.ctrl_ln1.aq1"), (LUMI, "lumi.ctrl_ln2.aq1")], ENDPOINTS: { diff --git a/zhaquirks/xiaomi/aqara/feeder_acn001.py b/zhaquirks/xiaomi/aqara/feeder_acn001.py index 49ed2b9ba3..54ec1707df 100644 --- a/zhaquirks/xiaomi/aqara/feeder_acn001.py +++ b/zhaquirks/xiaomi/aqara/feeder_acn001.py @@ -4,7 +4,7 @@ import logging from typing import Any -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha import zigpy.types as types from zigpy.zcl.clusters.general import ( Basic, @@ -266,8 +266,8 @@ class AqaraFeederAcn001(XiaomiCustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, @@ -296,8 +296,8 @@ class AqaraFeederAcn001(XiaomiCustomDevice): ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, diff --git a/zhaquirks/xiaomi/aqara/illumination.py b/zhaquirks/xiaomi/aqara/illumination.py index 0f732d028c..37e58688c7 100644 --- a/zhaquirks/xiaomi/aqara/illumination.py +++ b/zhaquirks/xiaomi/aqara/illumination.py @@ -1,8 +1,9 @@ -"""Quirk for lumi.sen_ill.mgl01 illumination sensor.""" +"""Quirk for Aqara illumination sensor.""" import logging from zigpy.profiles import zha -from zigpy.zcl.clusters.general import Basic, Identify +import zigpy.types as types +from zigpy.zcl.clusters.general import Basic, Identify, PowerConfiguration from zigpy.zcl.clusters.measurement import IlluminanceMeasurement from zigpy.zdo.types import NodeDescriptor @@ -16,18 +17,23 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.xiaomi import LUMI, BasicCluster, XiaomiCustomDevice +from zhaquirks.xiaomi import ( + LUMI, + BasicCluster, + XiaomiAqaraE1Cluster, + XiaomiCustomDevice, +) _LOGGER = logging.getLogger(__name__) class Illumination(XiaomiCustomDevice): - """Aqara LUMI lumi.sen_ill.mgl01.""" + """Aqara LUMI lumi.sen_ill.mgl01 illumination sensor.""" signature = { # MODELS_INFO: [(LUMI, "lumi.sen_ill.mgl01"), ("XIAOMI", "lumi.sen_ill.mgl01")], ENDPOINTS: { @@ -36,9 +42,9 @@ class Illumination(XiaomiCustomDevice): DEVICE_TYPE: zha.DeviceType.LIGHT_SENSOR, INPUT_CLUSTERS: [ Basic.cluster_id, + PowerConfiguration.cluster_id, Identify.cluster_id, IlluminanceMeasurement.cluster_id, - PowerConfigurationCluster.cluster_id, ], OUTPUT_CLUSTERS: [Identify.cluster_id], } @@ -63,3 +69,39 @@ class Illumination(XiaomiCustomDevice): } }, } + + +class OppleCluster(XiaomiAqaraE1Cluster): + """Opple cluster with configurable detection interval.""" + + ep_attribute = "opple_cluster" + attributes = { + 0x0000: ("detection_interval", types.uint16_t, True), + } + + +class IlluminationT1(XiaomiCustomDevice): + """Aqara LUMI T1 illumination sensor with configurable detection interval.""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.sen_ill.agl01")], + ENDPOINTS: Illumination.signature[ENDPOINTS], + } + + replacement = { + NODE_DESCRIPTOR: Illumination.replacement[NODE_DESCRIPTOR], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.LIGHT_SENSOR, + INPUT_CLUSTERS: [ + BasicCluster, + Identify.cluster_id, + IlluminanceMeasurement.cluster_id, + PowerConfigurationCluster, + OppleCluster, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id], + } + }, + } diff --git a/zhaquirks/xiaomi/aqara/motion_ac02.py b/zhaquirks/xiaomi/aqara/motion_ac02.py index 27085d7a5d..c44f254fd4 100644 --- a/zhaquirks/xiaomi/aqara/motion_ac02.py +++ b/zhaquirks/xiaomi/aqara/motion_ac02.py @@ -8,6 +8,7 @@ from zigpy.quirks import CustomDevice import zigpy.types as types from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration +from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing from zhaquirks import Bus, LocalDataCluster from zhaquirks.const import ( @@ -26,8 +27,6 @@ XiaomiPowerConfiguration, ) -OCCUPANCY = 0 -ON = 1 MOTION_ATTRIBUTE = 274 DETECTION_INTERVAL = 0x0102 MOTION_SENSITIVITY = 0x010C @@ -49,8 +48,13 @@ def _update_attribute(self, attrid: int, value: Any) -> None: super()._update_attribute(attrid, value) if attrid == MOTION_ATTRIBUTE: value = value - 65536 - self.endpoint.illuminance.illuminance_reported(value) - self.endpoint.occupancy.update_attribute(OCCUPANCY, ON) + self.endpoint.illuminance.update_attribute( + IlluminanceMeasurement.AttributeDefs.measured_value.id, value + ) + self.endpoint.occupancy.update_attribute( + OccupancySensing.AttributeDefs.occupancy.id, + OccupancySensing.Occupancy.Occupied, + ) async def write_attributes( self, attributes: dict[str | int, Any], manufacturer: int | None = None @@ -60,7 +64,7 @@ async def write_attributes( interval = attributes.get( "detection_interval", attributes.get(DETECTION_INTERVAL) ) - _LOGGER.debug("interval: %s", interval) + _LOGGER.debug("detection interval: %s", interval) if interval is not None: self.endpoint.ias_zone.reset_s = int(interval) return result @@ -69,24 +73,25 @@ async def write_attributes( class LocalIlluminanceMeasurementCluster( LocalDataCluster, IlluminanceMeasurementCluster ): - """Local lluminance measurement cluster.""" + """Local illuminance measurement cluster.""" def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) - if self.ATTR_ID not in self._attr_cache: + if self.AttributeDefs.measured_value.id not in self._attr_cache: # put a default value so the sensor is created - self._update_attribute(self.ATTR_ID, 0) + self._update_attribute(self.AttributeDefs.measured_value.id, 0) - def illuminance_reported(self, value): - """Illuminance reported.""" - if value < 0 or value > 0xFFDC: - _LOGGER.debug( + def _update_attribute(self, attrid, value): + if attrid == self.AttributeDefs.measured_value.id and ( + value < 0 or value > 0xFFDC + ): + self.debug( "Received invalid illuminance value: %s - setting illuminance to 0", value, ) value = 0 - super().illuminance_reported(value) + super()._update_attribute(attrid, value) class LocalOccupancyCluster(LocalDataCluster, OccupancyCluster): @@ -106,8 +111,6 @@ def __init__(self, *args, **kwargs): """Init.""" self.battery_size = 11 self.battery_quantity = 2 - self.battery_bus = Bus() - self.illuminance_bus = Bus() self.motion_bus = Bus() super().__init__(*args, **kwargs) diff --git a/zhaquirks/xiaomi/aqara/motion_agl02.py b/zhaquirks/xiaomi/aqara/motion_agl02.py index a0d6bdcfa2..b31e453952 100644 --- a/zhaquirks/xiaomi/aqara/motion_agl02.py +++ b/zhaquirks/xiaomi/aqara/motion_agl02.py @@ -44,7 +44,6 @@ def __init__(self, *args, **kwargs): """Init.""" self.battery_size = 11 self.motion_bus = Bus() - self.illuminance_bus = Bus() super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/motion_aq2.py b/zhaquirks/xiaomi/aqara/motion_aq2.py index fcaf042b76..e3647cc92c 100644 --- a/zhaquirks/xiaomi/aqara/motion_aq2.py +++ b/zhaquirks/xiaomi/aqara/motion_aq2.py @@ -38,7 +38,6 @@ def __init__(self, *args, **kwargs): """Init.""" self.battery_size = 9 self.motion_bus = Bus() - self.illuminance_bus = Bus() super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/motion_aq2b.py b/zhaquirks/xiaomi/aqara/motion_aq2b.py index d4042788b1..212b896d14 100644 --- a/zhaquirks/xiaomi/aqara/motion_aq2b.py +++ b/zhaquirks/xiaomi/aqara/motion_aq2b.py @@ -34,7 +34,6 @@ def __init__(self, *args, **kwargs): """Init.""" self.battery_size = 9 self.motion_bus = Bus() - self.illuminance_bus = Bus() super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/opple_switch.py b/zhaquirks/xiaomi/aqara/opple_switch.py index 46c2aaeaf1..5a08794195 100644 --- a/zhaquirks/xiaomi/aqara/opple_switch.py +++ b/zhaquirks/xiaomi/aqara/opple_switch.py @@ -3,7 +3,7 @@ from enum import Enum from zigpy import types as t -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.zcl.clusters.general import ( Alarms, AnalogInput, @@ -204,8 +204,8 @@ class XiaomiOpple2ButtonSwitchBase(XiaomiCustomDevice): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -354,8 +354,8 @@ class XiaomiOpple2ButtonSwitchFace1(XiaomiOpple2ButtonSwitchBase): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, @@ -401,8 +401,8 @@ class XiaomiOpple2ButtonSwitchFace2(XiaomiOpple2ButtonSwitchBase): OUTPUT_CLUSTERS: [], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, diff --git a/zhaquirks/xiaomi/aqara/plug.py b/zhaquirks/xiaomi/aqara/plug.py index 6c8d63bf72..547b68bee1 100644 --- a/zhaquirks/xiaomi/aqara/plug.py +++ b/zhaquirks/xiaomi/aqara/plug.py @@ -17,7 +17,6 @@ Time, ) -from zhaquirks import Bus from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -41,13 +40,6 @@ class Plug(XiaomiCustomDevice): """lumi.plug plug.""" - def __init__(self, *args, **kwargs): - """Init.""" - self.voltage_bus = Bus() - self.consumption_bus = Bus() - self.power_bus = Bus() - super().__init__(*args, **kwargs) - signature = { MODELS_INFO: [(LUMI, "lumi.plug")], ENDPOINTS: { diff --git a/zhaquirks/xiaomi/aqara/plug_eu.py b/zhaquirks/xiaomi/aqara/plug_eu.py index 515779ce01..cf1fd84126 100644 --- a/zhaquirks/xiaomi/aqara/plug_eu.py +++ b/zhaquirks/xiaomi/aqara/plug_eu.py @@ -2,7 +2,7 @@ import logging import zigpy -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha import zigpy.types as types from zigpy.zcl.clusters.general import ( Alarms, @@ -20,7 +20,6 @@ from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.smartenergy import Metering -from zhaquirks import Bus from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -41,8 +40,6 @@ _LOGGER = logging.getLogger(__name__) -XIAOMI_PROFILE_ID = 0xA1E0 -XIAOMI_DEVICE_TYPE = 0x61 OPPLE_MFG_CODE = 0x115F @@ -86,13 +83,6 @@ async def bind(self): class PlugMMEU01(XiaomiCustomDevice): """lumi.plug.mmeu01 plug.""" - def __init__(self, *args, **kwargs): - """Init.""" - self.voltage_bus = Bus() - self.consumption_bus = Bus() - self.power_bus = Bus() - super().__init__(*args, **kwargs) - signature = { MODELS_INFO: [ (LUMI, "lumi.plug.mmeu01"), @@ -123,8 +113,8 @@ def __init__(self, *args, **kwargs): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: XIAOMI_PROFILE_ID, - DEVICE_TYPE: XIAOMI_DEVICE_TYPE, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, @@ -155,8 +145,8 @@ def __init__(self, *args, **kwargs): INPUT_CLUSTERS: [AnalogInputCluster], }, 242: { - PROFILE_ID: XIAOMI_PROFILE_ID, - DEVICE_TYPE: XIAOMI_DEVICE_TYPE, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, @@ -210,8 +200,8 @@ class PlugMMEU01Alt1(PlugMMEU01): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: XIAOMI_PROFILE_ID, - DEVICE_TYPE: XIAOMI_DEVICE_TYPE, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, @@ -249,8 +239,8 @@ class PlugMMEU01Alt2(PlugMMEU01): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: XIAOMI_PROFILE_ID, - DEVICE_TYPE: XIAOMI_DEVICE_TYPE, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, @@ -306,8 +296,8 @@ class PlugMMEU01Alt3(PlugMMEU01): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: XIAOMI_PROFILE_ID, - DEVICE_TYPE: XIAOMI_DEVICE_TYPE, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], }, }, diff --git a/zhaquirks/xiaomi/aqara/plug_maus01.py b/zhaquirks/xiaomi/aqara/plug_maus01.py index 1d8132dee1..a11089110c 100644 --- a/zhaquirks/xiaomi/aqara/plug_maus01.py +++ b/zhaquirks/xiaomi/aqara/plug_maus01.py @@ -18,7 +18,6 @@ ) from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement -from zhaquirks import Bus from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -43,13 +42,6 @@ class Plug(XiaomiCustomDevice): """lumi.plug.maus01 plug.""" - def __init__(self, *args, **kwargs): - """Init.""" - self.voltage_bus = Bus() - self.consumption_bus = Bus() - self.power_bus = Bus() - super().__init__(*args, **kwargs) - signature = { MODELS_INFO: [(LUMI, "lumi.plug.maus01"), (LUMI, "lumi.plug.mitw01")], ENDPOINTS: { diff --git a/zhaquirks/xiaomi/aqara/relay_c2acn01.py b/zhaquirks/xiaomi/aqara/relay_c2acn01.py index ed0426e7a2..3d9353294c 100644 --- a/zhaquirks/xiaomi/aqara/relay_c2acn01.py +++ b/zhaquirks/xiaomi/aqara/relay_c2acn01.py @@ -17,7 +17,6 @@ ) from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement -from zhaquirks import Bus from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -42,13 +41,6 @@ class Relay(XiaomiCustomDevice): """lumi.relay.c2acn01 relay.""" - def __init__(self, *args, **kwargs): - """Init.""" - self.voltage_bus = Bus() - self.consumption_bus = Bus() - self.power_bus = Bus() - super().__init__(*args, **kwargs) - signature = { MODELS_INFO: [(LUMI, "lumi.relay.c2acn01")], ENDPOINTS: { diff --git a/zhaquirks/xiaomi/aqara/roller_curtain_e1.py b/zhaquirks/xiaomi/aqara/roller_curtain_e1.py index 0adb57970b..e962feb77c 100644 --- a/zhaquirks/xiaomi/aqara/roller_curtain_e1.py +++ b/zhaquirks/xiaomi/aqara/roller_curtain_e1.py @@ -4,7 +4,7 @@ from typing import Any from zigpy import types as t -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.zcl import foundation from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.general import ( @@ -18,13 +18,12 @@ MultistateOutput, OnOff, Ota, - PowerConfiguration, Scenes, Time, ) from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster -from zhaquirks import Bus, CustomCluster, LocalDataCluster +from zhaquirks import CustomCluster from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -33,7 +32,13 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.xiaomi import LUMI, BasicCluster, XiaomiCluster, XiaomiCustomDevice +from zhaquirks.xiaomi import ( + LUMI, + BasicCluster, + XiaomiCluster, + XiaomiCustomDevice, + XiaomiPowerConfiguration, +) PRESENT_VALUE = 0x0055 CURRENT_POSITION_LIFT_PERCENTAGE = 0x0008 @@ -100,7 +105,6 @@ async def command( *args: Any, manufacturer: int | t.uint16_t | None = None, expect_reply: bool = True, - tries: int = 1, tsn: int | t.uint8_t | None = None, **kwargs: Any, ) -> Any: @@ -153,32 +157,20 @@ class MultistateOutputRollerE1(CustomCluster, MultistateOutput): ) -class PowerConfigurationRollerE1(PowerConfiguration, LocalDataCluster): - """Xiaomi power configuration cluster implementation.""" - - BATTERY_PERCENTAGE_REMAINING = 0x0021 +class PowerConfigurationRollerE1(XiaomiPowerConfiguration): + """Power cluster which ignores Xiaomi voltage reports.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Init.""" - super().__init__(*args, **kwargs) - self.endpoint.device.power_bus_percentage.add_listener(self) - - def update_battery_percentage(self, value: int) -> None: - """Doubles the battery percentage to the Zigbee spec's expected 200% maximum.""" - super()._update_attribute( - self.BATTERY_PERCENTAGE_REMAINING, - (value * 2), - ) + def _update_battery_percentage(self, voltage_mv: int) -> None: + """Ignore Xiaomi voltage reports, so they're not used to calculate battery percentage.""" + # This device sends battery percentage reports which are handled using a XiaomiCluster and + # the inherited XiaomiPowerConfiguration cluster. + # This device might also send Xiaomi battery reports, so we only want to use those for the voltage attribute, + # but not for the battery percentage. XiaomiPowerConfiguration.battery_reported() still updates the voltage. class RollerE1AQ(XiaomiCustomDevice): """Aqara Roller Shade Driver E1 device.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Init.""" - self.power_bus_percentage: Bus = Bus() # type: ignore - super().__init__(*args, **kwargs) # type: ignore - signature = { MODELS_INFO: [(LUMI, "lumi.curtain.acn002")], ENDPOINTS: { @@ -212,8 +204,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, @@ -245,8 +237,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: ], }, 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, @@ -291,8 +283,8 @@ class RollerE1AQ_2(RollerE1AQ): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, @@ -337,8 +329,8 @@ class RollerE1AQ_3(RollerE1AQ): # input_clusters=[] # output_clusters=[33]> 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 0x0061, + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id, diff --git a/zhaquirks/xiaomi/aqara/sensor_ht_agl02.py b/zhaquirks/xiaomi/aqara/sensor_ht_agl02.py index 5bcc251289..332177ecc5 100644 --- a/zhaquirks/xiaomi/aqara/sensor_ht_agl02.py +++ b/zhaquirks/xiaomi/aqara/sensor_ht_agl02.py @@ -9,7 +9,6 @@ ) from zigpy.zdo.types import NodeDescriptor -from zhaquirks import Bus from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -32,13 +31,6 @@ class LumiSensorHtAgl02(XiaomiCustomDevice): """Lumi lumi.sensor_ht.agl02 custom device implementation.""" - def __init__(self, *args, **kwargs): - """Init.""" - self.temperature_bus = Bus() - self.humidity_bus = Bus() - self.pressure_bus = Bus() - super().__init__(*args, **kwargs) - signature = { MODELS_INFO: [("LUMI", "lumi.sensor_ht.agl02")], ENDPOINTS: { diff --git a/zhaquirks/xiaomi/aqara/smoke.py b/zhaquirks/xiaomi/aqara/smoke.py index 77f9c32a49..c66889744a 100644 --- a/zhaquirks/xiaomi/aqara/smoke.py +++ b/zhaquirks/xiaomi/aqara/smoke.py @@ -8,7 +8,7 @@ from zigpy.zcl.clusters.security import IasZone from zigpy.zdo.types import NodeDescriptor -from zhaquirks import Bus, LocalDataCluster +from zhaquirks import LocalDataCluster from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -84,11 +84,6 @@ class LocalIasZone(LocalDataCluster, IasZone): class LumiSensorSmokeAcn03(CustomDevice): """lumi.sensor_smoke.acn03 smoke sensor.""" - def __init__(self, *args, **kwargs): - """Init.""" - self.battery_bus = Bus() - super().__init__(*args, **kwargs) - signature = { MODELS_INFO: [(LUMI, "lumi.sensor_smoke.acn03")], ENDPOINTS: { diff --git a/zhaquirks/xiaomi/aqara/switch_h1.py b/zhaquirks/xiaomi/aqara/switch_h1.py new file mode 100644 index 0000000000..1932eb7910 --- /dev/null +++ b/zhaquirks/xiaomi/aqara/switch_h1.py @@ -0,0 +1,226 @@ +from zigpy.profiles import zgp, zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Alarms, + Basic, + GreenPowerProxy, + Groups, + Identify, + OnOff, + Ota, + Scenes, + Time, +) + +from zhaquirks.const import ( + ARGS, + ATTR_ID, + BUTTON, + CLUSTER_ID, + COMMAND, + COMMAND_DOUBLE, + COMMAND_HOLD, + COMMAND_SINGLE, + DEVICE_TYPE, + DOUBLE_PRESS, + ENDPOINT_ID, + ENDPOINTS, + INPUT_CLUSTERS, + LONG_PRESS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PRESS_TYPE, + PROFILE_ID, + SHORT_PRESS, + VALUE, +) +from zhaquirks.xiaomi import ( + LUMI, + BasicCluster, + DeviceTemperatureCluster, + OnOffCluster, + XiaomiMeteringCluster, +) +from zhaquirks.xiaomi.aqara.opple_remote import MultistateInputCluster +from zhaquirks.xiaomi.aqara.opple_switch import OppleSwitchCluster + +XIAOMI_COMMAND_SINGLE = "41_single" +XIAOMI_COMMAND_DOUBLE = "41_double" +XIAOMI_COMMAND_HOLD = "1_hold" + + +class AqaraH1SingleRockerBase(CustomDevice): + """Device automation triggers for the Aqara H1 Single Rocker Switches""" + + device_automation_triggers = { + (SHORT_PRESS, BUTTON): { + ENDPOINT_ID: 41, + CLUSTER_ID: 18, + COMMAND: XIAOMI_COMMAND_SINGLE, + ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_SINGLE, VALUE: 1}, + }, + (DOUBLE_PRESS, BUTTON): { + ENDPOINT_ID: 41, + CLUSTER_ID: 18, + COMMAND: XIAOMI_COMMAND_DOUBLE, + ARGS: {ATTR_ID: 0x0055, PRESS_TYPE: COMMAND_DOUBLE, VALUE: 2}, + }, + (LONG_PRESS, BUTTON): { + ENDPOINT_ID: 1, + CLUSTER_ID: 64704, + COMMAND: XIAOMI_COMMAND_HOLD, + ARGS: {ATTR_ID: 0x00FC, PRESS_TYPE: COMMAND_HOLD, VALUE: 0}, + }, + } + + +class AqaraH1SingleRockerSwitchWithNeutral(AqaraH1SingleRockerBase): + """Aqara H1 Single Rocker Switch (with neutral).""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.switch.n1aeu1")], + ENDPOINTS: { + # input_clusters=[0, 2, 3, 4, 5, 6, 18, 64704], output_clusters=[10, 25] + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, # 0 + DeviceTemperatureCluster.cluster_id, # 2 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + OnOff.cluster_id, # 6 + Alarms.cluster_id, # 9 + XiaomiMeteringCluster.cluster_id, # 0x0702 + 0x0B04, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, # 0x000a + Ota.cluster_id, # 0x0019 + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, # 0x0021 + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + BasicCluster, # 0 + DeviceTemperatureCluster.cluster_id, # 2 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + OnOffCluster, # 6 + Alarms.cluster_id, # 9 + MultistateInputCluster, # 18 + XiaomiMeteringCluster.cluster_id, # 0x0702 + OppleSwitchCluster, # 0xFCC0 / 64704 + 0x0B04, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 41: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + MultistateInputCluster, # 18 + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + +class AqaraH1SingleRockerSwitchNoNeutral(AqaraH1SingleRockerBase): + """Aqara H1 Single Rocker Switch (no neutral).""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.switch.l1aeu1")], + ENDPOINTS: { + # input_clusters=[0, 2, 3, 4, 5, 6, 9], output_clusters=[10, 25] + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, # 0 + DeviceTemperatureCluster.cluster_id, # 2 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + OnOff.cluster_id, # 6 + Alarms.cluster_id, # 9 + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, # 0x000a + Ota.cluster_id, # 0x0019 + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, # 0x0021 + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + BasicCluster, # 0 + DeviceTemperatureCluster.cluster_id, # 2 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + OnOffCluster, # 6 + Alarms.cluster_id, # 9 + MultistateInputCluster, # 18 + OppleSwitchCluster, # 0xFCC0 / 64704 + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 41: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + MultistateInputCluster, # 18 + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } diff --git a/zhaquirks/xiaomi/aqara/switch_t1.py b/zhaquirks/xiaomi/aqara/switch_t1.py new file mode 100644 index 0000000000..63ac57c363 --- /dev/null +++ b/zhaquirks/xiaomi/aqara/switch_t1.py @@ -0,0 +1,202 @@ +"""Aqara T1 (with neutral) relays.""" +from zigpy import types as t +from zigpy.profiles import zgp, zha +from zigpy.zcl.clusters.general import ( + Alarms, + AnalogInput, + Basic, + DeviceTemperature, + GreenPowerProxy, + Groups, + Identify, + MultistateInput, + OnOff, + Ota, + Scenes, + Time, +) + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.xiaomi import ( + LUMI, + AnalogInputCluster, + BasicCluster, + DeviceTemperatureCluster, + ElectricalMeasurementCluster, + MeteringCluster, + XiaomiAqaraE1Cluster, + XiaomiCustomDevice, +) +from zhaquirks.xiaomi.aqara.opple_remote import MultistateInputCluster + + +class OppleCluster(XiaomiAqaraE1Cluster): + """Xiaomi Aqara T1 relay cluster.""" + + ep_attribute = "opple_cluster" + attributes = { + 0x000A: ("switch_type", t.uint8_t, True), + 0x0201: ("power_outage_memory", t.Bool, True), + 0x0203: ("do_not_disturb", t.Bool, True), + } + + +class SwitchT1(XiaomiCustomDevice): + """Aqara T1 relay switch.""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.switch.n0agl1"), (LUMI, "lumi.switch.n0acn2")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + Alarms.cluster_id, + Time.cluster_id, + OppleCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + 21: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 31: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 41: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [MultistateInput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + BasicCluster, + DeviceTemperatureCluster, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Alarms.cluster_id, + Time.cluster_id, + MeteringCluster, + ElectricalMeasurementCluster, + OppleCluster, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + ], + }, + 21: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [AnalogInputCluster], + OUTPUT_CLUSTERS: [], + }, + 31: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 41: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [MultistateInputCluster], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + } + } + + +class SwitchT1Alt1(SwitchT1): + """Aqara T1 relay switch with alternative signature.""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.switch.n0agl1"), (LUMI, "lumi.switch.n0acn2")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + Alarms.cluster_id, + Time.cluster_id, + OppleCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + 0xFFFF, + ], + }, + 21: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 31: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 41: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [MultistateInput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } diff --git a/zhaquirks/xiaomi/aqara/thermostat_agl001.py b/zhaquirks/xiaomi/aqara/thermostat_agl001.py index 6eb4eccf1d..ff6a63b068 100644 --- a/zhaquirks/xiaomi/aqara/thermostat_agl001.py +++ b/zhaquirks/xiaomi/aqara/thermostat_agl001.py @@ -387,9 +387,7 @@ class AqaraThermostatSpecificCluster(XiaomiAqaraE1Cluster): def _update_attribute(self, attrid, value): self.debug("Updating attribute on Xiaomi cluster %s with %s", attrid, value) if attrid == BATTERY_PERCENTAGE: - self.endpoint.device.battery_bus.listener_event( - "battery_percent_reported", value - ) + self.endpoint.power.battery_percent_reported(value) elif attrid == SYSTEM_MODE: # update ZCL system_mode attribute (e.g. on attribute reports) self.endpoint.thermostat.update_attribute( diff --git a/zhaquirks/xiaomi/aqara/tvoc.py b/zhaquirks/xiaomi/aqara/tvoc.py index 52609f67ce..34058e1c33 100644 --- a/zhaquirks/xiaomi/aqara/tvoc.py +++ b/zhaquirks/xiaomi/aqara/tvoc.py @@ -8,7 +8,7 @@ from zigpy.zcl.clusters.security import IasZone from zigpy.zdo.types import NodeDescriptor -from zhaquirks import Bus, LocalDataCluster, PowerConfigurationCluster +from zhaquirks import LocalDataCluster, PowerConfigurationCluster from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -28,6 +28,7 @@ ) MEASURED_VALUE = 0x0000 +DISPLAY_UNIT = 0x0114 class AnalogInputCluster(CustomCluster, AnalogInput): @@ -66,15 +67,27 @@ async def bind(self): return result +class TVOCDisplayUnit(t.enum_factory(t.uint8_t)): + """Display values.""" + + mgm3_celsius = 0x00 + ppb_celsius = 0x01 + mgm3_fahrenheit = 0x10 + ppb_fahrenheit = 0x11 + + +class TVOCCluster(XiaomiAqaraE1Cluster): + """Aqara LUMI Config cluster.""" + + ep_attribute = "aqara_cluster" + attributes = { + DISPLAY_UNIT: ("display_unit", TVOCDisplayUnit, True), + } + + class TVOCMonitor(XiaomiCustomDevice): """Aqara LUMI lumi.airmonitor.acn01.""" - def __init__(self, *args, **kwargs): - """Init.""" - self.temperature_bus = Bus() - self.humidity_bus = Bus() - super().__init__(*args, **kwargs) - signature = { #