Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
eaafd53
Migrate Centralite quirks
puddly Nov 15, 2025
3f50223
Migrate quirks, pass 1
puddly Nov 15, 2025
ec59a69
Migrate quirks, pass 2
puddly Nov 15, 2025
0720fb3
Migrate quirks, pass 3
puddly Nov 15, 2025
937c133
Migrate quirks, pass 4
puddly Nov 15, 2025
c07d89b
Exclude Samjin multi sensor from Centralite quirk
puddly Nov 15, 2025
33a3da6
Migrate quirks, pass 5
puddly Nov 15, 2025
e91ae43
Migrate Aqara curtain motor
puddly Nov 15, 2025
d7743ee
Migrate quirks, pass 6 (IKEA)
puddly Nov 15, 2025
1445b1e
Migrate quirks, pass 7
puddly Nov 15, 2025
aba1ed6
Migrate quirks, pass 8
puddly Nov 15, 2025
df139eb
Migrate quirks, pass 9
puddly Nov 15, 2025
3debdc0
Migrate quirks, pass 10
puddly Nov 15, 2025
6ca1a42
Migrate quirks, pass 11
puddly Nov 15, 2025
c2eea14
Migrate quirks, pass 12
puddly Nov 15, 2025
d90e275
Migrate quirks, pass 13
puddly Nov 15, 2025
1a54db8
Migrate quirks, pass 14
puddly Nov 15, 2025
c400572
Migrate quirks, pass 15
puddly Nov 15, 2025
72c772b
Migrate quirks, pass 16
puddly Nov 15, 2025
a04aae4
Migrate quirks, pass 17
puddly Nov 15, 2025
3ad04be
Migrate quirks, pass 18
puddly Nov 15, 2025
dbbfd36
Migrate quirks, pass 19
puddly Nov 16, 2025
f69faaa
Migrate quirks, pass 20
puddly Nov 16, 2025
3c86ed8
Migrate quirks, pass 21
puddly Nov 16, 2025
b2c2116
Migrate quirks, pass 22
puddly Nov 16, 2025
b06a25c
Migrate quirks, pass 23
puddly Nov 16, 2025
06d8dde
Update KOF quirk with device info from ZHA database
puddly Nov 17, 2025
3963b6a
Fix typo in Konke quirk
puddly Nov 17, 2025
07cc63c
Fix Aqara E1 driver
puddly Nov 17, 2025
8c3f4e1
More fixes
puddly Nov 17, 2025
a18dc5f
Use v2 device class for Xiaomi
puddly Nov 17, 2025
c45a4ad
Migrate quirks, pass 24
puddly Nov 17, 2025
8a99192
Migrate quirks, pass 25
puddly Nov 17, 2025
d6c8ee2
Migrate quirks, pass 26
puddly Nov 17, 2025
34f93a6
WIP: Fix most failing tests
puddly Nov 17, 2025
1edfc69
Fix up bad quirks
puddly Nov 17, 2025
fe152a6
Ignore custom quirk loading test
puddly Nov 17, 2025
8fb5e10
Fix KOF quirks
puddly Nov 17, 2025
2af0d41
Minimize diff
puddly Nov 17, 2025
8cd54e0
Pre-commit
puddly Nov 17, 2025
b84d607
Bump zigpy to get rid of direction warning
puddly Nov 17, 2025
259e574
Regenerate `uv.lock`
puddly Nov 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ authors = [{ name = "David F. Mulcahey", email = "david.mulcahey@icloud.com" }]
readme = "README.md"
license = { text = "Apache-2.0" }
requires-python = ">=3.12"
dependencies = ["zigpy>=0.85.0"]
dependencies = ["zigpy>=0.86.0"]

[tool.setuptools.packages.find]
exclude = ["tests", "tests.*"]
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ def _dev(
for cluster_id in out_clusters:
ep.add_output_cluster(cluster_id)
return raw_device
else:
assert isinstance(quirked, zigpy.quirks.BaseCustomDevice)

MockAppController.devices[ieee] = quirked

Expand Down
44 changes: 6 additions & 38 deletions tests/test_danfoss.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,10 @@
zhaquirks.setup()


def test_popp_signature(assert_signature_matches_quirk):
"""Test the signature matching the Device Class."""
signature = {
"node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=4678, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *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)",
# SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=769, device_version=1, input_clusters=[0, 1, 3, 10, 32, 513, 516, 2821], output_clusters=[0, 25])
"endpoints": {
"1": {
"profile_id": 260,
"device_type": "0x0301",
"in_clusters": [
"0x0000",
"0x0001",
"0x0003",
"0x000a",
"0x0020",
"0x0201",
"0x0204",
"0x0b05",
],
"out_clusters": ["0x0000", "0x0019"],
}
},
"manufacturer": "D5X84YU",
"model": "eT093WRO",
"class": "danfoss.thermostat.DanfossThermostat",
}

assert_signature_matches_quirk(
zhaquirks.danfoss.thermostat.DanfossThermostat, signature
)


@mock.patch("zigpy.zcl.Cluster.bind", mock.AsyncMock())
async def test_danfoss_time_bind(zigpy_device_from_quirk):
async def test_danfoss_time_bind(zigpy_device_from_v2_quirk):
"""Test the time being set when binding the Time cluster."""
device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat)
device = zigpy_device_from_v2_quirk("Danfoss", "eTRV0103")

danfoss_time_cluster = device.endpoints[1].time
danfoss_thermostat_cluster = device.endpoints[1].thermostat
Expand All @@ -74,9 +42,9 @@ def mock_write(attributes, manufacturer=None):
assert 0x0002 in danfoss_time_cluster._attr_cache


async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk):
async def test_danfoss_thermostat_write_attributes(zigpy_device_from_v2_quirk):
"""Test the Thermostat writes behaving correctly, in particular regarding setpoint."""
device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat)
device = zigpy_device_from_v2_quirk("Danfoss", "eTRV0103")

danfoss_thermostat_cluster = device.endpoints[1].thermostat

Expand Down Expand Up @@ -144,12 +112,12 @@ def mock_setpoint(oper, sett, manufacturer=None):
assert setting == 5


async def test_customized_standardcluster(zigpy_device_from_quirk):
async def test_customized_standardcluster(zigpy_device_from_v2_quirk):
"""Test customized standard cluster class correctly separating zigbee operations.

This is regarding manufacturer specific attributes.
"""
device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat)
device = zigpy_device_from_v2_quirk("Danfoss", "eTRV0103")

danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id]

Expand Down
36 changes: 0 additions & 36 deletions tests/test_gledopto.py
Original file line number Diff line number Diff line change
@@ -1,37 +1 @@
"""Tests for GLEDOPTO quirks."""

import zhaquirks.gledopto.glc009


def test_gledopto_glc009_signature(assert_signature_matches_quirk):
"""Test GLEDOPTO GL-C-009 signature is matched to its quirk."""
signature = {
"node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=0, maximum_buffer_size=80, maximum_incoming_transfer_size=160, server_mask=0, maximum_outgoing_transfer_size=160, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *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": {
"11": {
"profile_id": 49246,
"device_type": "0x0100",
"in_clusters": [
"0x0000",
"0x0003",
"0x0004",
"0x0005",
"0x0006",
"0x0008",
"0x0300",
],
"out_clusters": [],
},
"13": {
"profile_id": 49246,
"device_type": "0x0100",
"in_clusters": ["0x1000"],
"out_clusters": ["0x1000"],
},
},
"manufacturer": "GLEDOPTO",
"model": "GL-C-009",
"class": "zigpy.device.Device",
}

assert_signature_matches_quirk(zhaquirks.gledopto.glc009.GLC009, signature)
87 changes: 10 additions & 77 deletions tests/test_ikea.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,77 +15,6 @@
zhaquirks.setup()


def test_ikea_starkvind(assert_signature_matches_quirk):
"""Test new 'STARKVIND Air purifier table' signature is matched to its quirk."""

signature = {
"node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4476, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *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": "0x0007",
"in_clusters": [
"0x0000",
"0x0003",
"0x0004",
"0x0005",
"0x0202",
"0xfc57",
"0xfc7d",
],
"out_clusters": ["0x0019", "0x0400", "0x042a"],
},
"242": {
"profile_id": 41440,
"device_type": "0x0061",
"in_clusters": [],
"out_clusters": ["0x0021"],
},
},
"manufacturer": "IKEA of Sweden",
"model": "STARKVIND Air purifier",
"class": "ikea.starkvind.IkeaSTARKVIND",
}

assert_signature_matches_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND, signature)


def test_ikea_starkvind_v2(assert_signature_matches_quirk):
"""Test new 'STARKVIND Air purifier table' signature is matched to its quirk."""

signature = {
"node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4476, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *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": "0x0007",
"in_clusters": [
"0x0000",
"0x0003",
"0x0004",
"0x0005",
"0x0202",
"0xfc57",
"0xfc7c",
"0xfc7d",
],
"out_clusters": ["0x0019", "0x0400", "0x042a"],
},
"242": {
"profile_id": 41440,
"device_type": "0x0061",
"in_clusters": [],
"out_clusters": ["0x0021"],
},
},
"manufacturer": "IKEA of Sweden",
"model": "STARKVIND Air purifier table",
"class": "ikea.starkvind.IkeaSTARKVIND_v2",
}

assert_signature_matches_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND_v2, signature)


@pytest.mark.parametrize("attribute", ["fan_speed", "fan_mode"])
@pytest.mark.parametrize(
"value,expected",
Expand All @@ -98,11 +27,13 @@ def test_ikea_starkvind_v2(assert_signature_matches_quirk):
],
)
async def test_fan_speed_mode_update(
zigpy_device_from_quirk, attribute, value, expected
zigpy_device_from_v2_quirk, attribute, value, expected
):
"""Test reading the fan speed and mode."""

starkvind_device = zigpy_device_from_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND)
starkvind_device = zigpy_device_from_v2_quirk(
"IKEA of Sweden", "STARKVIND Air purifier"
)
assert starkvind_device.model == "STARKVIND Air purifier"

ikea_cluster = starkvind_device.endpoints[1].in_clusters[
Expand All @@ -117,10 +48,12 @@ async def test_fan_speed_mode_update(
assert ikea_listener.attribute_updates[0] == (attr_id, expected)


async def test_pm25_cluster_read(zigpy_device_from_quirk):
async def test_pm25_cluster_read(zigpy_device_from_v2_quirk):
"""Test reading from PM25 cluster."""

starkvind_device = zigpy_device_from_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND)
starkvind_device = zigpy_device_from_v2_quirk(
"IKEA of Sweden", "STARKVIND Air purifier"
)
assert starkvind_device.model == "STARKVIND Air purifier"

pm25_cluster = starkvind_device.endpoints[1].in_clusters[PM25.cluster_id]
Expand Down Expand Up @@ -175,7 +108,7 @@ def mock_read(attributes, manufacturer=None):
)
async def test_double_power_config_firmware(
caplog,
zigpy_device_from_quirk,
zigpy_device_from_v2_quirk,
firmware,
pct_device,
pct_correct,
Expand All @@ -184,7 +117,7 @@ async def test_double_power_config_firmware(
):
"""Test battery percentage remaining is doubled for old firmware."""

device = zigpy_device_from_quirk(zhaquirks.ikea.fivebtnremote.IkeaTradfriRemote1)
device = zigpy_device_from_v2_quirk("IKEA of Sweden", "TRADFRI remote control")

basic_cluster = device.endpoints[1].basic
ClusterListener(basic_cluster)
Expand Down
16 changes: 8 additions & 8 deletions tests/test_konke.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@


@pytest.mark.parametrize(
"quirk", (zhaquirks.konke.motion.KonkeMotion, zhaquirks.konke.motion.KonkeMotionB)
"model", ("3AFE28010402000D", "3AFE14010402000D", "3AFE27010402000D")
)
async def test_konke_motion(zigpy_device_from_quirk, quirk):
async def test_konke_motion(zigpy_device_from_v2_quirk, model):
"""Test konke motion sensor."""

motion_dev = zigpy_device_from_quirk(quirk)
motion_dev = zigpy_device_from_v2_quirk("Konke", model)

motion_cluster = motion_dev.endpoints[1].ias_zone
motion_listener = ClusterListener(motion_cluster)
Expand Down Expand Up @@ -67,16 +67,16 @@ async def test_konke_motion(zigpy_device_from_quirk, quirk):


@pytest.mark.parametrize(
"quirk",
"model",
(
zhaquirks.konke.button.KonkeButtonRemote1,
zhaquirks.konke.button.KonkeButtonRemote2,
"3AFE170100510001",
"3AFE280100510001",
),
)
async def test_konke_button(zigpy_device_from_quirk, quirk):
async def test_konke_button(zigpy_device_from_v2_quirk, model):
"""Test Konke button remotes."""

device = zigpy_device_from_quirk(quirk)
device = zigpy_device_from_v2_quirk("Konke", model)
cluster = device.endpoints[1].konke_on_off

listener = mock.MagicMock()
Expand Down
4 changes: 2 additions & 2 deletions tests/test_legrand.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
(24.0, 0), # below min
),
)
async def test_legrand_battery(zigpy_device_from_quirk, voltage, bpr):
async def test_legrand_battery(zigpy_device_from_v2_quirk, voltage, bpr):
"""Test Legrand battery voltage to % battery left."""

device = zigpy_device_from_quirk(zhaquirks.legrand.dimmer.RemoteDimmer)
device = zigpy_device_from_v2_quirk(f" {LEGRAND}", " Remote dimmer switch")
power_cluster = device.endpoints[1].power
power_cluster.update_attribute(0x0020, voltage)
assert power_cluster["battery_percentage_remaining"] == bpr
Expand Down
8 changes: 5 additions & 3 deletions tests/test_linkind.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
zhaquirks.setup()


@pytest.mark.parametrize("quirk", (zhaquirks.linkind.motion.LinkindD0003,))
async def test_linkind_motion_ignore_alarm_2(zigpy_device_from_quirk, quirk):
@pytest.mark.parametrize("manufacturer,model", [("lk", "ZB-MotionSensor-D0003")])
async def test_linkind_motion_ignore_alarm_2(
zigpy_device_from_v2_quirk, manufacturer, model
):
"""Test that the quirk for the Linkind motion sensor ignores the IasZone Alarm_2 bit."""
device = zigpy_device_from_quirk(quirk)
device = zigpy_device_from_v2_quirk(manufacturer, model)

ias_zone_cluster = device.endpoints[1].ias_zone
ias_zone_listener = ClusterListener(ias_zone_cluster)
Expand Down
8 changes: 4 additions & 4 deletions tests/test_linxura.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
zhaquirks.setup()


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

device = zigpy_device_from_quirk(zhaquirks.linxura.button.LinxuraButton)
device = zigpy_device_from_v2_quirk("Linxura", "Smart Controller")
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id
cluster = device.endpoints[1].ias_zone
listener = mock.MagicMock()
Expand Down Expand Up @@ -99,9 +99,9 @@ async def test_button_ias(zigpy_device_from_quirk):
),
],
)
async def test_button_triggers(zigpy_device_from_quirk, message, button, press_type):
async def test_button_triggers(zigpy_device_from_v2_quirk, message, button, press_type):
"""Test ZHA_SEND_EVENT case."""
device = zigpy_device_from_quirk(zhaquirks.linxura.button.LinxuraButton)
device = zigpy_device_from_v2_quirk("Linxura", "Smart Controller")
cluster = device.endpoints[1].ias_zone
listener = mock.MagicMock()
cluster.add_listener(listener)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_orvibo.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
zhaquirks.setup()


@pytest.mark.parametrize("quirk", (zhaquirks.orvibo.motion.SN10ZW,))
async def test_orvibo_motion(zigpy_device_from_quirk, quirk):
@pytest.mark.parametrize("model", ("895a2d80097f4ae2b2d40500d5e03dcc",))
async def test_orvibo_motion(zigpy_device_from_v2_quirk, model):
"""Test Orvibo motion sensor."""

motion_dev = zigpy_device_from_quirk(quirk)
motion_dev = zigpy_device_from_v2_quirk("ORVIBO", model)

motion_cluster = motion_dev.endpoints[1].ias_zone
motion_listener = ClusterListener(motion_cluster)
Expand Down
Loading
Loading