Skip to content

Commit

Permalink
Merge branch 'dev' into add_aqaraH2Plug
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristophCaina authored Dec 12, 2024
2 parents 1f1f978 + 5f1a7f2 commit 2b2d3c2
Show file tree
Hide file tree
Showing 96 changed files with 8,755 additions and 2,313 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ jobs:
PYTHON_VERSION_DEFAULT: 3.12
PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit
MINIMUM_COVERAGE_PERCENTAGE: 80
PYTHON_MATRIX: "3.12"
PYTHON_MATRIX: '"3.12", "3.13"'
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
11 changes: 8 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
ci:
autofix_commit_msg: "Apply pre-commit auto fixes"
autoupdate_commit_msg: "Auto-update pre-commit hooks"
skip: [mypy]

repos:
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies: [tomli]
args: ["--toml", "pyproject.toml"]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
rev: v1.13.0
hooks:
- id: mypy

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.2
rev: v0.8.0
hooks:
- id: ruff
args: ["--fix", "--exit-non-zero-on-fix", "--config", "pyproject.toml"]
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ ZHA device handlers bridge the functionality gap created when manufacturers devi

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.


For supporting Tuya devices, see [using the TuyaQuirkBuilder](/tuya.md).

# How to contribute

## Primer
Expand Down Expand Up @@ -397,7 +400,7 @@ replacement = {

You can see that we have replaced `ElectricalMeasurement.cluster_id` from endpoint 1 in the `signature` dict with the name of our cluster that we created: `ElectricalMeasurementCluster` and on endpoint 2 we replaced `AnalogInput.cluster_id` with the implementation we created for that: `AnalogInputCluster`. This instructs Zigpy to use these `CustomCluster` derivatives instead of the normal cluster definitions for these clusters and this is why this part of the quirk is called `replacement`.

Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the culsters that we wanted to replace with the `CustomCluster` implementations that we created.
Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the clusters that we wanted to replace with the `CustomCluster` implementations that we created.

# Contribution Guidelines

Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ authors = [
{name = "David F. Mulcahey", email = "[email protected]"}
]
readme = "README.md"
license = {text = "Apache License Version 2.0"}
license = {text = "Apache-2.0"}
requires-python = ">=3.12"
dependencies = [
"zigpy>=0.65.2",
"zigpy>=0.70.0",
]

[tool.setuptools.packages.find]
Expand Down Expand Up @@ -79,7 +79,7 @@ norecursedirs = ".git testing_config"

[tool.codespell]
skip = "Contributors.md"
ignore-words-list = "hass, dout, potentiels"
ignore-words-list = "hass, dout, potentiels, checkin"
quiet-level = 2

[tool.ruff]
Expand Down Expand Up @@ -218,4 +218,4 @@ split-on-trailing-comma = false
"script/*" = ["T20"]

[tool.ruff.lint.mccabe]
max-complexity = 27
max-complexity = 27
4 changes: 2 additions & 2 deletions requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ black
flake8
codecov
colorlog
codespell
codespell>=2.3.0
coveralls
mypy==0.942
pre-commit
Expand All @@ -13,5 +13,5 @@ pytest-sugar
pytest-timeout
pytest-asyncio
pytest>=7.1.3
zigpy>=0.65.2
zigpy>=0.70
ruff==0.0.261
43 changes: 43 additions & 0 deletions tests/async_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Mock utilities that are async aware."""

from unittest.mock import * # noqa: F401, F403


class _IntSentinelObject(int):
"""Sentinel-like object that is also an integer subclass.
Allows sentinels to be used
in loggers that perform int-specific string formatting.
"""

def __new__(cls, name):
instance = super().__new__(cls, 0)
instance.name = name
return instance

def __repr__(self):
return f"int_sentinel.{self.name}"

def __hash__(self):
return hash((int(self), self.name))

def __eq__(self, other):
return self is other

__str__ = __reduce__ = __repr__


class _IntSentinel:
def __init__(self):
self._sentinels = {}

def __getattr__(self, name):
if name == "__bases__":
raise AttributeError
return self._sentinels.setdefault(name, _IntSentinelObject(name))

def __reduce__(self):
return "int_sentinel"


int_sentinel = _IntSentinel()
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ async def add_endpoint(self, descriptor):
def app_controller_mock():
"""App controller mock."""
config = {"device": {"path": "/dev/ttyUSB0"}, "database": None}
config = MockApp.SCHEMA(config)
app = MockApp(config)
return app

Expand Down
5 changes: 3 additions & 2 deletions tests/test_danfoss.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest import mock

from zigpy.quirks import CustomCluster
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters.hvac import Thermostat
from zigpy.zcl.foundation import WriteAttributesStatusRecord, ZCLAttributeDef
Expand Down Expand Up @@ -161,8 +162,8 @@ async def test_customized_standardcluster(zigpy_device_from_quirk):
) == [[4545, 345], [5433, 45355]]

mock_attributes = {
656: ZCLAttributeDef(is_manufacturer_specific=True),
56454: ZCLAttributeDef(is_manufacturer_specific=False),
656: ZCLAttributeDef(type=t.uint8_t, is_manufacturer_specific=True),
56454: ZCLAttributeDef(type=t.uint8_t, is_manufacturer_specific=False),
}

danfoss_thermostat_cluster.attributes = mock_attributes
Expand Down
116 changes: 116 additions & 0 deletions tests/test_linxura.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Tests for Linxura quirks."""

from unittest import mock

import pytest
from zigpy.zcl.clusters.security import IasZone

import zhaquirks
import zhaquirks.linxura

zhaquirks.setup()


async def test_button_ias(zigpy_device_from_quirk):
"""Test Linxura button remotes."""

device = zigpy_device_from_quirk(zhaquirks.linxura.button.LinxuraButton)
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id
cluster = device.endpoints[1].ias_zone
listener = mock.MagicMock()
cluster.add_listener(listener)

for i in range(0, 24):
# button press
cluster.update_attribute(ias_zone_status_attr_id, i)

# update_attribute on the IasZone cluster is always called
assert listener.attribute_updated.call_args[0][0] == ias_zone_status_attr_id
assert listener.attribute_updated.call_args[0][1] == i

# we get 20 events, 4 are discarded as invalid (0, 6, 12, 18)
assert listener.attribute_updated.call_count == 24
assert listener.zha_send_event.call_count == 20


@pytest.mark.parametrize(
"message, button, press_type",
[
(
b"\x18\n\n\x02\x00\x19\x01\x00\xfe\xff0\x01",
"button_1",
"remote_button_short_press",
),
(
b"\x18\n\n\x02\x00\x19\x03\x00\xfe\xff0\x01",
"button_1",
"remote_button_double_press",
),
(
b"\x18\n\n\x02\x00\x19\x05\x00\xfe\xff0\x01",
"button_1",
"remote_button_long_press",
),
(
b"\x18\n\n\x02\x00\x19\x07\x00\xfe\xff0\x01",
"button_2",
"remote_button_short_press",
),
(
b"\x18\n\n\x02\x00\x19\x09\x00\xfe\xff0\x01",
"button_2",
"remote_button_double_press",
),
(
b"\x18\n\n\x02\x00\x19\x0b\x00\xfe\xff0\x01",
"button_2",
"remote_button_long_press",
),
(
b"\x18\n\n\x02\x00\x19\x0d\x00\xfe\xff0\x01",
"button_3",
"remote_button_short_press",
),
(
b"\x18\n\n\x02\x00\x19\x0f\x00\xfe\xff0\x01",
"button_3",
"remote_button_double_press",
),
(
b"\x18\n\n\x02\x00\x19\x11\x00\xfe\xff0\x01",
"button_3",
"remote_button_long_press",
),
(
b"\x18\n\n\x02\x00\x19\x13\x00\xfe\xff0\x01",
"button_4",
"remote_button_short_press",
),
(
b"\x18\n\n\x02\x00\x19\x15\x00\xfe\xff0\x01",
"button_4",
"remote_button_double_press",
),
(
b"\x18\n\n\x02\x00\x19\x17\x00\xfe\xff0\x01",
"button_4",
"remote_button_long_press",
),
],
)
async def test_button_triggers(zigpy_device_from_quirk, message, button, press_type):
"""Test ZHA_SEND_EVENT case."""
device = zigpy_device_from_quirk(zhaquirks.linxura.button.LinxuraButton)
cluster = device.endpoints[1].ias_zone
listener = mock.MagicMock()
cluster.add_listener(listener)

device.handle_message(260, cluster.cluster_id, 1, 1, message)
assert listener.zha_send_event.call_count == 1
assert listener.zha_send_event.call_args == mock.call(
f"{button}_{press_type}",
{
"button": button,
"press_type": press_type,
},
)
Loading

0 comments on commit 2b2d3c2

Please sign in to comment.