From ad2eba0de34f68dcd6204aa4119387f800d79ec1 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Mon, 21 Nov 2022 20:50:13 +0100 Subject: [PATCH 01/12] Add `_TZE200_yjjdcqsq` Tuya sensor (#1951) * Add `_TZE200_yjjdcqsq` Tuya sensor Add new device signature for the `_TZE200_yjjdcqsq` sensor Fix: #1944 * Restore removed clusters No sure if are functional but I don't like to remove without knowing. --- zhaquirks/tuya/ts0601_sensor.py | 48 +++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/zhaquirks/tuya/ts0601_sensor.py b/zhaquirks/tuya/ts0601_sensor.py index e450ed2c59..f0bd98a631 100644 --- a/zhaquirks/tuya/ts0601_sensor.py +++ b/zhaquirks/tuya/ts0601_sensor.py @@ -1,4 +1,4 @@ -"""Tuya temp and humidity sensor with e-ink screen.""" +"""Tuya temp and humidity sensors.""" from typing import Any, Dict @@ -25,7 +25,7 @@ class TuyaTemperatureMeasurement(TemperatureMeasurement, TuyaLocalCluster): class TuyaRelativeHumidity(RelativeHumidity, TuyaLocalCluster): - """Tuya local RelativeHumidity cluster.""" + """Tuya local RelativeHumidity cluster with a device RH_MULTIPLIER factor.""" def update_attribute(self, attr_name: str, value: Any) -> None: """Apply a correction factor to value.""" @@ -162,3 +162,47 @@ class TuyaTempHumiditySensor_Square(CustomDevice): } }, } + + +class TuyaTempHumiditySensorVar03(CustomDevice): + """Tuya temp and humidity sensor (variation 03).""" + + signature = { + # "profile_id": 260, + # "device_type": "0x0051", + # "in_clusters": ["0x0000","0x0004","0x0005","0xef00"], + # "out_clusters": ["0x000a","0x0019"] + MODELS_INFO: [("_TZE200_yjjdcqsq", "TS0601")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TemperatureHumidityManufCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + } + }, + } + + replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TemperatureHumidityManufCluster, + TuyaTemperatureMeasurement, + TuyaRelativeHumidity, + TuyaPowerConfigurationCluster2AAA, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + } + }, + } From f6205fa6d1dd5165e0b76262d7e56010fc8727e3 Mon Sep 17 00:00:00 2001 From: KislitsaEA Date: Tue, 29 Nov 2022 23:38:32 +0300 Subject: [PATCH 02/12] Update ts0601_dimmer.py (#1971) Added manufacturer support _TZE200_p0gzbqct for Tuya TS0601 dimmer --- zhaquirks/tuya/ts0601_dimmer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index 8af11a97f1..94bfa7c0bb 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -48,6 +48,7 @@ class TuyaSingleSwitchDimmer(TuyaDimmerSwitch): ("_TZE200_la2c2uo9", "TS0601"), ("_TZE200_1agwnems", "TS0601"), # TODO: validation pending? ("_TZE200_9cxuhakf", "TS0601"), # Added for Mercator IKUU SSWM-DIMZ Device + ("_TZE200_p0gzbqct", "TS0601"), ], ENDPOINTS: { # Date: Tue, 29 Nov 2022 21:38:53 +0100 Subject: [PATCH 03/12] New `TS0012` variation (#1967) New quirk for `TS0012` devices. Will support `_TZ3000_jl7qyupf` Fixes: #1898 --- zhaquirks/tuya/ts001x.py | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/zhaquirks/tuya/ts001x.py b/zhaquirks/tuya/ts001x.py index 4331954009..e9dd4cbecf 100644 --- a/zhaquirks/tuya/ts001x.py +++ b/zhaquirks/tuya/ts001x.py @@ -517,6 +517,76 @@ class Tuya_Double_No_N_Plus(TuyaSwitch): } +class Tuya_Double_Var05(TuyaSwitch): + """Tuya 2 gang no neutral light switch (variation 05).""" + + signature = { + MODEL: "TS0012", + ENDPOINTS: { + 1: { + # "profile_id": 260, + # "device_type": "0x0100", + # "in_clusters": ["0x0000","0x0003","0x0004","0x0005","0x0006","0xe000","0xe001"], + # "out_clusters": ["0x000a","0x0019"] + 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: [Ota.cluster_id, Time.cluster_id], + }, + 2: { + # "profile_id": 260, + # "device_type": "0x0100", + # "in_clusters": ["0x0004","0x0005","0x0006"], + # "out_clusters": [] + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + }, + } + 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: [ + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + }, + } + + class Tuya_Triple_No_N(TuyaSwitch): """Tuya 3 gang no neutral light switch.""" From d105fb7c296f1b70d02b0a9bed29548201e126ca Mon Sep 17 00:00:00 2001 From: Leissson <98490303+Leissson@users.noreply.github.com> Date: Sun, 4 Dec 2022 23:00:40 +0200 Subject: [PATCH 04/12] Update ts0601_smoke.py (#1978) --- zhaquirks/tuya/ts0601_smoke.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhaquirks/tuya/ts0601_smoke.py b/zhaquirks/tuya/ts0601_smoke.py index c9470e4481..fc1697d534 100644 --- a/zhaquirks/tuya/ts0601_smoke.py +++ b/zhaquirks/tuya/ts0601_smoke.py @@ -79,6 +79,7 @@ def __init__(self, *args, **kwargs): ("_TZE200_aycxwiau", "TS0601"), ("_TZE200_ntcy3xu1", "TS0601"), ("_TZE200_vzekyi4c", "TS0601"), + ("_TZE200_dq1mfjug", "TS0601"), ], ENDPOINTS: { 1: { From 2323715dad3477463ead502adf2dad761fcd44a5 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 4 Dec 2022 22:01:23 +0100 Subject: [PATCH 05/12] Add the TS0003 and TS0004 metering devices (#1983) And here comes the TS0003 and TS0004 devices with the metering cluster. Fixes: #1981 Fixes: #1982 --- zhaquirks/tuya/ts000x.py | 245 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) diff --git a/zhaquirks/tuya/ts000x.py b/zhaquirks/tuya/ts000x.py index 3b9497325a..f3a8546a04 100644 --- a/zhaquirks/tuya/ts000x.py +++ b/zhaquirks/tuya/ts000x.py @@ -436,6 +436,117 @@ class Switch_3G_GPP(EnchantedDevice, CustomDevice): } +class Switch_3G_Metering(EnchantedDevice, CustomDevice): + """Tuya 3 gang switch with metering support.""" + + 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, + TuyaZBMeteringCluster.cluster_id, + TuyaZBElectricalMeasurement.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: [ + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + # + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + # + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + 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, + TuyaZBMeteringCluster, + TuyaZBElectricalMeasurement, + 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: [ + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + class Switch_4G_GPP(EnchantedDevice, CustomDevice): """Tuya 4 gang switch module with restore power state support.""" @@ -574,3 +685,137 @@ class Switch_4G_GPP(EnchantedDevice, CustomDevice): }, }, } + + +class Switch_4G_Metering(EnchantedDevice, CustomDevice): + """Tuya 4 gang switch with metering support.""" + + 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, + TuyaZBMeteringCluster.cluster_id, + TuyaZBElectricalMeasurement.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: [ + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + # + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + # + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + # + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + 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, + TuyaZBMeteringCluster, + TuyaZBElectricalMeasurement, + 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: [ + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } From 71e5589fff84c983fec5135af162a9a489ac8f28 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 4 Dec 2022 22:01:55 +0100 Subject: [PATCH 06/12] Add debug traces to `handle_set_time_request` (#1985) Some debug logs to help in issues analysis --- zhaquirks/tuya/mcu/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhaquirks/tuya/mcu/__init__.py b/zhaquirks/tuya/mcu/__init__.py index caa5bc1709..dd650eed19 100644 --- a/zhaquirks/tuya/mcu/__init__.py +++ b/zhaquirks/tuya/mcu/__init__.py @@ -279,6 +279,7 @@ def handle_mcu_version_response(self, payload: MCUVersion) -> foundation.Status: def handle_set_time_request(self, payload: t.uint16_t) -> foundation.Status: """Handle set_time requests (0x24).""" + self.debug("handle_set_time_request payload: %s", payload) payload_rsp = TuyaTimePayload() utc_now = datetime.datetime.utcnow() @@ -295,6 +296,7 @@ def handle_set_time_request(self, payload: t.uint16_t) -> foundation.Status: payload_rsp.extend(utc_timestamp.to_bytes(4, "big", signed=False)) payload_rsp.extend(local_timestamp.to_bytes(4, "big", signed=False)) + self.debug("handle_set_time_request response: %s", payload_rsp) self.create_catching_task( super().command(TUYA_SET_TIME, payload_rsp, expect_reply=False) ) From 77c596a38e3cc67e06a5d51fffcbc4d127e954ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20B=C5=82a=C5=BCewicz?= Date: Sun, 4 Dec 2022 22:04:18 +0100 Subject: [PATCH 07/12] Update supported Python versions (#1950) --- .github/workflows/ci.yml | 92 +++++++++++++-------------- .github/workflows/publish-to-pypi.yml | 8 +-- requirements_test_all.txt | 2 +- setup.cfg | 2 +- setup.py | 4 +- tox.ini | 2 +- 6 files changed, 55 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5baa92a0ad..087698217d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: env: CACHE_VERSION: 1 - DEFAULT_PYTHON: '3.9.14' + PYTHON_VERSION_DEFAULT: '3.9.15' PRE_COMMIT_HOME: ~/.cache/pre-commit jobs: @@ -21,18 +21,18 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8.14', '3.9.14', '3.10.7'] + python-version: ['3.8.14', '3.9.15', '3.10.8', '3.11.0'] steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: venv key: >- @@ -57,15 +57,15 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/checkout@v3 + - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} + uses: actions/setup-python@v4 id: python with: - python-version: ${{ env.DEFAULT_PYTHON }} + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: venv key: >- @@ -80,7 +80,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -99,15 +99,15 @@ jobs: needs: pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v2 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/checkout@v3 + - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} + uses: actions/setup-python@v4 id: python with: - python-version: ${{ env.DEFAULT_PYTHON }} + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: venv key: >- @@ -122,7 +122,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -143,15 +143,15 @@ jobs: needs: pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v2 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/checkout@v3 + - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} + uses: actions/setup-python@v4 id: python with: - python-version: ${{ env.DEFAULT_PYTHON }} + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: venv key: >- @@ -166,7 +166,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -190,15 +190,15 @@ jobs: needs: pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v2 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/checkout@v3 + - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} + uses: actions/setup-python@v4 id: python with: - python-version: ${{ env.DEFAULT_PYTHON }} + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: venv key: >- @@ -213,7 +213,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -234,15 +234,15 @@ jobs: needs: pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v2 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/checkout@v3 + - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} + uses: actions/setup-python@v4 id: python with: - python-version: ${{ env.DEFAULT_PYTHON }} + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: venv key: >- @@ -257,7 +257,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.PRE_COMMIT_HOME }} key: | @@ -280,20 +280,20 @@ jobs: needs: prepare-base strategy: matrix: - python-version: ['3.8.14', '3.9.14', '3.10.7'] + python-version: ['3.8.14', '3.9.15', '3.10.8', '3.11.0'] name: >- Run tests Python ${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v4 id: python with: python-version: ${{ matrix.python-version }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: venv key: >- @@ -328,7 +328,7 @@ jobs: -p no:sugar \ tests - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.0 + uses: actions/upload-artifact@v3 with: name: coverage-${{ matrix.python-version }} path: .coverage @@ -347,15 +347,15 @@ jobs: needs: pytest steps: - name: Check out code from GitHub - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v4 id: python with: - python-version: ${{ env.DEFAULT_PYTHON }} + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: venv key: >- @@ -369,7 +369,7 @@ jobs: echo "Failed to restore Python virtual environment from cache" exit 1 - name: Download all coverage artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 - name: Combine coverage results run: | . venv/bin/activate @@ -377,7 +377,7 @@ jobs: coverage report --fail-under=72 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 - name: Upload coverage to Coveralls env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 0854b15eb0..dbb66f13c1 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -6,11 +6,11 @@ jobs: name: Build and publish distributions to PyPI and TestPyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: Set up Python 3.6 - uses: actions/setup-python@v1 + - uses: actions/checkout@v3 + - name: Set up Python 3.9 + uses: actions/setup-python@v4 with: - version: 3.6 + version: 3.9 - name: Install wheel run: >- pip install wheel diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5893916fa2..0c0911c66e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,4 +13,4 @@ pytest-sugar pytest-timeout pytest-asyncio pytest>=7.1.3 -zigpy>=0.51.6 +zigpy>=0.52 diff --git a/setup.cfg b/setup.cfg index 9d49392528..103b7e7e3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ line_length = 88 indent = " " [mypy] -python_version = 3.7 +python_version = 3.9 check_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_calls = true diff --git a/setup.py b/setup.py index edf8f19188..a37605bcb0 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ license="Apache License Version 2.0", keywords="zha quirks homeassistant hass", packages=find_packages(exclude=["tests"]), - python_requires=">=3", - install_requires=["zigpy>=0.51.6"], + python_requires=">=3.8", + install_requires=["zigpy>=0.52"], tests_require=["pytest"], ) diff --git a/tox.ini b/tox.ini index ee42f5b7e1..c3502b1923 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38, py39, py310, lint, black +envlist = py38, py39, py310, py311, lint, black skip_missing_interpreters = True [testenv] From ad9ad3321c00b80f9cb8152b970ee9bb49274499 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 4 Dec 2022 22:04:41 +0100 Subject: [PATCH 08/12] Fix TuyaDPType.VALUE payload (#1984) --- tests/test_tuya_dimmer.py | 4 ++-- tests/test_tuya_mcu.py | 2 +- tests/test_tuya_valve.py | 2 +- zhaquirks/tuya/mcu/__init__.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_tuya_dimmer.py b/tests/test_tuya_dimmer.py index bd08cae7ef..97679ef2a4 100644 --- a/tests/test_tuya_dimmer.py +++ b/tests/test_tuya_dimmer.py @@ -48,7 +48,7 @@ async def test_command(zigpy_device_from_quirk, quirk): m1.assert_called_with( 61184, 4, - b"\x01\x04\x00\x00\x03\x02\x02\x00\x04\x00\x00\x03r", + b"\x01\x04\x00\x00\x03\x02\x02\x00\x04r\x03\x00\x00", expect_reply=True, command_id=0, ) @@ -78,7 +78,7 @@ async def test_write_attr(zigpy_device_from_quirk, quirk): m1.assert_called_with( 61184, 2, - b"\x01\x02\x00\x00\x01\x03\x02\x00\x04\x00\x00\x00b", + b"\x01\x02\x00\x00\x01\x03\x02\x00\x04b\x00\x00\x00", expect_reply=False, command_id=0, ) diff --git a/tests/test_tuya_mcu.py b/tests/test_tuya_mcu.py index be1d25d582..0bd1ea2580 100644 --- a/tests/test_tuya_mcu.py +++ b/tests/test_tuya_mcu.py @@ -135,7 +135,7 @@ async def test_tuya_methods(zigpy_device_from_quirk, quirk): assert len(result_1.datapoints) == 1 assert result_1.datapoints[0].dp == 9 assert result_1.datapoints[0].data.dp_type == TuyaDPType.VALUE - assert result_1.datapoints[0].data.raw == b"\x00\x00\x00b" + assert result_1.datapoints[0].data.raw == b"b\x00\x00\x00" tcd_2 = TuyaClusterData( endpoint_id=7, cluster_attr="not_exists_attribute", attr_value=25 diff --git a/tests/test_tuya_valve.py b/tests/test_tuya_valve.py index d07ded2f80..f4a7785a8a 100644 --- a/tests/test_tuya_valve.py +++ b/tests/test_tuya_valve.py @@ -61,7 +61,7 @@ async def test_write_attr_psbzs(zigpy_device_from_quirk, quirk): m1.assert_called_with( 61184, 2, - b"\x01\x02\x00\x00\x01\x05\x02\x00\x04\x00\x00\x00\x0f", + b"\x01\x02\x00\x00\x01\x05\x02\x00\x04\x0f\x00\x00\x00", expect_reply=False, command_id=0, ) diff --git a/zhaquirks/tuya/mcu/__init__.py b/zhaquirks/tuya/mcu/__init__.py index dd650eed19..ad9718ae45 100644 --- a/zhaquirks/tuya/mcu/__init__.py +++ b/zhaquirks/tuya/mcu/__init__.py @@ -217,7 +217,7 @@ def from_cluster_data(self, data: TuyaClusterData) -> Optional[TuyaCommand]: tuya_data = TuyaData() tuya_data.dp_type = datapoint_type tuya_data.function = 0 - tuya_data.raw = t.LVBytes.deserialize(val)[0] + tuya_data.raw = bytes(reversed(val[1:])) self.debug("raw: %s", tuya_data.raw) dpd = TuyaDatapointData(dp, tuya_data) cmd_payload.datapoints = [dpd] From 566141e8afd80156c81230f35a0cd27d12c7869a Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Sun, 4 Dec 2022 22:07:15 +0100 Subject: [PATCH 09/12] Add power configuration cluster to ZLinky_TIC (requires firmware v12.0+) (#1962) * Add power configuration cluster to ZLinky_TIC (requires firmware 12+) * Power configuration cluster of ZLinky_TIC is apparently not declared * Add ZLinky_TIC_FW_V12 quirk with power configuration cluster in signature for ZLinky_TIC with firmware v12.0+ * Simplify code * Do not alter model info, it has to match the value from the basic cluster's model attribute --- zhaquirks/lixee/zlinky.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/zhaquirks/lixee/zlinky.py b/zhaquirks/lixee/zlinky.py index c4a88a3d99..cbfb662d07 100644 --- a/zhaquirks/lixee/zlinky.py +++ b/zhaquirks/lixee/zlinky.py @@ -1,8 +1,16 @@ """Quirk for ZLinky_TIC.""" +from copy import deepcopy + from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t -from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Identify, Ota +from zigpy.zcl.clusters.general import ( + Basic, + GreenPowerProxy, + Identify, + Ota, + PowerConfiguration, +) from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement, MeterIdentification from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster from zigpy.zcl.clusters.smartenergy import Metering @@ -170,6 +178,7 @@ class ZLinkyTIC(CustomDevice): DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, INPUT_CLUSTERS: [ Basic.cluster_id, + PowerConfiguration.cluster_id, Identify.cluster_id, ZLinkyTICMetering, MeterIdentification.cluster_id, @@ -186,3 +195,12 @@ class ZLinkyTIC(CustomDevice): }, }, } + + +class ZLinkyTICFWV12(ZLinkyTIC): + """ZLinky_TIC from LiXee with firmware v12.0+.""" + + signature = deepcopy(ZLinkyTIC.signature) + + # Insert PowerConfiguration cluster in signature for devices with firmware v12.0+ + signature[ENDPOINTS][1][INPUT_CLUSTERS].insert(1, PowerConfiguration.cluster_id) From 95007bba9d9995e16d147ca699200a8839fabd76 Mon Sep 17 00:00:00 2001 From: weihuan1111 <99949392+weihuan1111@users.noreply.github.com> Date: Mon, 5 Dec 2022 20:00:55 +0800 Subject: [PATCH 10/12] update (#1988) Change-Id: I5372b68cb0788f545c4e1e0887680a3e8f296ced --- zhaquirks/thirdreality/button.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zhaquirks/thirdreality/button.py b/zhaquirks/thirdreality/button.py index ef95064223..76053b1565 100644 --- a/zhaquirks/thirdreality/button.py +++ b/zhaquirks/thirdreality/button.py @@ -97,7 +97,11 @@ class Button(CustomDevice): CustomPowerConfigurationCluster, MultistateInputCluster, ], - OUTPUT_CLUSTERS: [Basic.cluster_id, OnOff.cluster_id], + OUTPUT_CLUSTERS: [ + OnOff.cluster_id, + LevelControl.cluster_id, + Ota.cluster_id, + ], } }, } From 265e7e457f5a9cf386cab82ffe8fd87fe212bf9c Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Wed, 7 Dec 2022 14:50:50 +0300 Subject: [PATCH 11/12] tuya mcu enhancements (#1989) * tuya mcu enhancements * python3.9 compatibility fix * fix to update multiple datapoints on attribute write * lint * reverse byte order of TuyaReverseStruct due to #1984 * apply suggestions from code review --- tests/test_tuya_mcu.py | 11 ++--- zhaquirks/tuya/__init__.py | 31 +++++++++++-- zhaquirks/tuya/mcu/__init__.py | 80 +++++++++++++++++++++++----------- 3 files changed, 89 insertions(+), 33 deletions(-) diff --git a/tests/test_tuya_mcu.py b/tests/test_tuya_mcu.py index 0bd1ea2580..4b4d6df0d6 100644 --- a/tests/test_tuya_mcu.py +++ b/tests/test_tuya_mcu.py @@ -131,11 +131,12 @@ async def test_tuya_methods(zigpy_device_from_quirk, quirk): result_1 = tuya_cluster.from_cluster_data(tcd_1) assert result_1 - assert result_1.datapoints - assert len(result_1.datapoints) == 1 - assert result_1.datapoints[0].dp == 9 - assert result_1.datapoints[0].data.dp_type == TuyaDPType.VALUE - assert result_1.datapoints[0].data.raw == b"b\x00\x00\x00" + assert len(result_1) == 1 + assert result_1[0].datapoints + assert len(result_1[0].datapoints) == 1 + assert result_1[0].datapoints[0].dp == 9 + assert result_1[0].datapoints[0].data.dp_type == TuyaDPType.VALUE + assert result_1[0].datapoints[0].data.raw == b"b\x00\x00\x00" tcd_2 = TuyaClusterData( endpoint_id=7, cluster_attr="not_exists_attribute", attr_value=25 diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index 420ad239db..65632df771 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -179,7 +179,12 @@ def deserialize(cls, data: bytes) -> Tuple["TuyaData", bytes]: res.dp_type, data = TuyaDPType.deserialize(data) res.function, data = t.uint8_t.deserialize(data) res.raw, data = t.LVBytes.deserialize(data) - if res.dp_type not in (TuyaDPType.BITMAP, TuyaDPType.STRING, TuyaDPType.ENUM): + if res.dp_type not in ( + TuyaDPType.BITMAP, + TuyaDPType.STRING, + TuyaDPType.ENUM, + TuyaDPType.RAW, + ): res.raw = res.raw[::-1] return res, data @@ -200,6 +205,8 @@ def payload(self) -> Union[t.Bool, t.CharacterString, t.uint32_t, t.data32]: return bitmaps[len(self.raw)].deserialize(self.raw)[0] except KeyError as exc: raise ValueError(f"Wrong bitmap length: {len(self.raw)}") from exc + elif self.dp_type == TuyaDPType.RAW: + return self.raw raise ValueError(f"Unknown {self.dp_type} datapoint type") @@ -1325,7 +1332,7 @@ class DPToAttributeMapping: """Container for datapoint to cluster attribute update mapping.""" ep_attribute: str - attribute_name: str + attribute_name: Union[str, tuple] converter: Optional[ Callable[ [ @@ -1337,6 +1344,14 @@ class DPToAttributeMapping: endpoint_id: Optional[int] = None +@dataclasses.dataclass +class AttributeWithMask: + """Container for the attribute and its mask.""" + + value: Any + mask: int + + class TuyaNewManufCluster(CustomCluster): """Tuya manufacturer specific cluster. @@ -1466,4 +1481,14 @@ def _dp_2_attr_update(self, datapoint: TuyaDatapointData) -> None: if dp_map.converter: value = dp_map.converter(value) - cluster.update_attribute(dp_map.attribute_name, value) + if isinstance(dp_map.attribute_name, tuple): + for k, v in zip(dp_map.attribute_name, value): + if isinstance(v, AttributeWithMask): + v = cluster.get(k, 0) & (~v.mask) | v.value + cluster.update_attribute(k, v) + else: + if isinstance(value, AttributeWithMask): + value = ( + cluster.get(dp_map.attribute_name, 0) & (~value.mask) | value.value + ) + cluster.update_attribute(dp_map.attribute_name, value) diff --git a/zhaquirks/tuya/mcu/__init__.py b/zhaquirks/tuya/mcu/__init__.py index ad9718ae45..8956dbf60d 100644 --- a/zhaquirks/tuya/mcu/__init__.py +++ b/zhaquirks/tuya/mcu/__init__.py @@ -53,7 +53,7 @@ class DPToAttributeMapping: """Container for datapoint to cluster attribute update mapping.""" ep_attribute: str - attribute_name: str + attribute_name: Union[str, tuple] dp_type: TuyaDPType converter: Optional[ Callable[ @@ -115,6 +115,8 @@ def read_attributes( async def write_attributes(self, attributes, manufacturer=None): """Defer attributes writing to the set_data tuya command.""" + await super().write_attributes(attributes, manufacturer) + records = self._write_attr_records(attributes) for record in records: @@ -196,9 +198,18 @@ def __init__(self, *args, **kwargs): def from_cluster_data(self, data: TuyaClusterData) -> Optional[TuyaCommand]: """Convert from cluster data to a tuya data payload.""" - dp, mapping = self.get_dp_mapping(data.endpoint_id, data.cluster_attr) - self.debug("from_cluster_data: %s, %s", dp, mapping) - if dp: + dp_mapping = self.get_dp_mapping(data.endpoint_id, data.cluster_attr) + self.debug("from_cluster_data: %s", dp_mapping) + if len(dp_mapping) == 0: + self.warning( + "No cluster_dp found for %s, %s", + data.endpoint_id, + data.cluster_attr, + ) + return [] + + tuya_commands = [] + for dp, mapping in dp_mapping.items(): cmd_payload = TuyaCommand() cmd_payload.status = 0 cmd_payload.tsn = self.endpoint.device.application.get_sequence() @@ -206,7 +217,19 @@ def from_cluster_data(self, data: TuyaClusterData) -> Optional[TuyaCommand]: datapoint_type = mapping.dp_type val = data.attr_value if mapping.dp_converter: - val = mapping.dp_converter(val) + args = [] + if isinstance(mapping.attribute_name, tuple): + endpoint = self.endpoint + if mapping.endpoint_id: + endpoint = endpoint.device.endpoints[mapping.endpoint_id] + cluster = getattr(endpoint, mapping.ep_attribute) + for attr in mapping.attribute_name: + args.append( + val if attr == data.cluster_attr else cluster.get(attr) + ) + else: + args.append(val) + val = mapping.dp_converter(*args) self.debug("converted: %s", val) if datapoint_type.ztype: val = datapoint_type.ztype(val) @@ -222,14 +245,8 @@ def from_cluster_data(self, data: TuyaClusterData) -> Optional[TuyaCommand]: dpd = TuyaDatapointData(dp, tuya_data) cmd_payload.datapoints = [dpd] - return cmd_payload - else: - self.warning( - "No cluster_dp found for %s, %s", - data.endpoint_id, - data.cluster_attr, - ) - return None + tuya_commands.append(cmd_payload) + return tuya_commands def tuya_mcu_command(self, cluster_data: TuyaClusterData): """Tuya MCU command listener. Only manufacturer endpoint must listen to MCU commands.""" @@ -239,9 +256,16 @@ def tuya_mcu_command(self, cluster_data: TuyaClusterData): cluster_data, ) - tuya_command = self.from_cluster_data(cluster_data) - self.debug("tuya_command: %s", tuya_command) - if tuya_command: + tuya_commands = self.from_cluster_data(cluster_data) + self.debug("tuya_commands: %s", tuya_commands) + if len(tuya_commands) == 0: + self.warning( + "no MCU command for data %s", + cluster_data, + ) + return + + for tuya_command in tuya_commands: self.create_catching_task( self.command( TUYA_SET_DATA, @@ -250,24 +274,30 @@ def tuya_mcu_command(self, cluster_data: TuyaClusterData): manufacturer=cluster_data.manufacturer, ) ) - else: - self.warning( - "no MCU command for data %s", - cluster_data, - ) def get_dp_mapping( self, endpoint_id: int, attribute_name: str ) -> Optional[Tuple[int, DPToAttributeMapping]]: """Search for the DP in dp_to_attribute.""" + result = {} for dp, dp_mapping in self.dp_to_attribute.items(): - if (attribute_name == dp_mapping.attribute_name) and ( - endpoint_id in [dp_mapping.endpoint_id, self.endpoint.endpoint_id] + if ( + attribute_name == dp_mapping.attribute_name + or ( + isinstance(dp_mapping.attribute_name, tuple) + and attribute_name in dp_mapping.attribute_name + ) + ) and ( + ( + dp_mapping.endpoint_id is None + and endpoint_id == self.endpoint.endpoint_id + ) + or (endpoint_id == dp_mapping.endpoint_id) ): self.debug("get_dp_mapping --> found DP: %s", dp) - return [dp, dp_mapping] - return [None, None] + result[dp] = dp_mapping + return result def handle_mcu_version_response(self, payload: MCUVersion) -> foundation.Status: """Handle MCU version response.""" From e832985d1d3d7d53d5884d65e2413a387a0bffaf Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 7 Dec 2022 06:51:46 -0500 Subject: [PATCH 12/12] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a37605bcb0..4ef48177e5 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.87" +VERSION = "0.0.88" setup(