zigpy 1.5.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zigpy-1.5.0/zigpy.egg-info → zigpy-1.6.0}/PKG-INFO +1 -1
- {zigpy-1.5.0 → zigpy-1.6.0}/pyproject.toml +3 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_quirks.py +28 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zcl.py +126 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/application.py +10 -1
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/__init__.py +4 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/providers.py +31 -12
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/__init__.py +13 -4
- {zigpy-1.5.0 → zigpy-1.6.0/zigpy.egg-info}/PKG-INFO +1 -1
- {zigpy-1.5.0 → zigpy-1.6.0}/COPYING +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/LICENSE +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/README.md +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/setup.cfg +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/setup.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_app_state.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_appdb.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_appdb_migration.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_application.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_backups.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_config.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_datastructures.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_datastructures_cpython.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_device.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_endpoint.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_event.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_group.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_listeners.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_packet_callbacks.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_quirks_registry.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_quirks_v2.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_serial.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_struct.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_topology.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_types.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zcl_clusters.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zcl_foundation.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zcl_helpers.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zdo.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zdo_types.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zigbee_util.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tools/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/tools/regenerate_mypy_ignores.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v0.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v1.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v10.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v11.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v12.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v13.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v14.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v15.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v2.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v3.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v4.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v5.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v6.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v7.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v8.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v9.sql +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/backports/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/backups.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/config/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/config/defaults.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/config/validators.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/const.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/datastructures.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/device.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/endpoint.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/event/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/event/event_base.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/exceptions.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/group.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/listeners.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/image.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/json_schemas.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/manager.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/validators.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/profiles/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/profiles/zgp.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/profiles/zha.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/profiles/zll.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/py.typed +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/registry.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/homeassistant/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/homeassistant/binary_sensor.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/homeassistant/number.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/homeassistant/sensor.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/serial.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/state.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/topology.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/types/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/types/basic.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/types/named.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/types/struct.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/typing.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/util.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/closures.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/general.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/general_const.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/greenpower.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/homeautomation.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/hvac.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/lighting.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/lightlink.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/manufacturer_specific.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/measurement.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/protocol.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/security.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/smartenergy.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/wwah.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/foundation.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/helpers.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zdo/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zdo/types.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zgp/__init__.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zgp/types.py +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy.egg-info/SOURCES.txt +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy.egg-info/dependency_links.txt +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy.egg-info/requires.txt +0 -0
- {zigpy-1.5.0 → zigpy-1.6.0}/zigpy.egg-info/top_level.txt +0 -0
|
@@ -36,6 +36,9 @@ zigpy = ["py.typed"]
|
|
|
36
36
|
[dependency-groups]
|
|
37
37
|
testing = [
|
|
38
38
|
"aioresponses",
|
|
39
|
+
# aioresponses 0.7.8 is incompatible with aiohttp 3.14's mandatory
|
|
40
|
+
# `stream_writer` argument; remove once aioresponses PR #288 is released.
|
|
41
|
+
"aiohttp<3.14",
|
|
39
42
|
"asynctest",
|
|
40
43
|
"codespell==2.4.1",
|
|
41
44
|
"coverage[toml]",
|
|
@@ -965,6 +965,34 @@ async def test_request_with_kwargs(real_device):
|
|
|
965
965
|
assert all(c == request_mock.mock_calls[0] for c in request_mock.mock_calls)
|
|
966
966
|
|
|
967
967
|
|
|
968
|
+
def test_custom_cluster_subclass_keeps_same_id_attributes() -> None:
|
|
969
|
+
"""Subclassing a custom cluster with two same-ID attributes keeps both."""
|
|
970
|
+
|
|
971
|
+
class MeteringBase(zigpy.quirks.CustomCluster, zcl.clusters.smartenergy.Metering):
|
|
972
|
+
class AttributeDefs(zcl.clusters.smartenergy.Metering.AttributeDefs):
|
|
973
|
+
# Shares its ID with the standard `current_summ_delivered` attribute
|
|
974
|
+
current_summ_delivered_mfg = zcl.foundation.ZCLAttributeDef(
|
|
975
|
+
id=0x0000,
|
|
976
|
+
type=t.uint48_t,
|
|
977
|
+
manufacturer_code=0x1166,
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
class MeteringVariant1(MeteringBase):
|
|
981
|
+
pass
|
|
982
|
+
|
|
983
|
+
class MeteringVariant2(MeteringBase):
|
|
984
|
+
pass
|
|
985
|
+
|
|
986
|
+
for cls in (MeteringBase, MeteringVariant1, MeteringVariant2):
|
|
987
|
+
assert "current_summ_delivered" in cls.attributes_by_name
|
|
988
|
+
assert "current_summ_delivered_mfg" in cls.attributes_by_name
|
|
989
|
+
assert cls.find_attribute("current_summ_delivered").id == 0x0000
|
|
990
|
+
assert (
|
|
991
|
+
cls.find_attribute(0x0000, manufacturer_code=0x1166)
|
|
992
|
+
is cls.AttributeDefs.current_summ_delivered_mfg
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
968
996
|
def test_purge_custom_quirks(tmp_path: pathlib.Path, app_mock) -> None:
|
|
969
997
|
def load_quirks():
|
|
970
998
|
for importer, modname, _ in pkgutil.walk_packages(path=[str(tmp_path)]):
|
|
@@ -1423,6 +1423,132 @@ async def test_zcl_cluster_definition_backwards_compatibility():
|
|
|
1423
1423
|
)
|
|
1424
1424
|
|
|
1425
1425
|
|
|
1426
|
+
def test_zcl_cluster_subclass_keeps_same_id_attributes():
|
|
1427
|
+
"""Subclassing a cluster keeps two attribute definitions sharing an ID."""
|
|
1428
|
+
|
|
1429
|
+
class TestCluster(zcl.Cluster):
|
|
1430
|
+
cluster_id = 0xABCD
|
|
1431
|
+
ep_attribute = "test_cluster"
|
|
1432
|
+
_skip_registry = True
|
|
1433
|
+
|
|
1434
|
+
class AttributeDefs(zcl.BaseAttributeDefs):
|
|
1435
|
+
attribute = foundation.ZCLAttributeDef(id=0x0001, type=t.uint8_t)
|
|
1436
|
+
attribute_mfg = foundation.ZCLAttributeDef(
|
|
1437
|
+
id=0x0001,
|
|
1438
|
+
type=t.uint48_t,
|
|
1439
|
+
manufacturer_code=0x1234,
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
class TestClusterSubclass(TestCluster):
|
|
1443
|
+
pass
|
|
1444
|
+
|
|
1445
|
+
class TestClusterSubSubclass(TestClusterSubclass):
|
|
1446
|
+
pass
|
|
1447
|
+
|
|
1448
|
+
for cls in (TestCluster, TestClusterSubclass, TestClusterSubSubclass):
|
|
1449
|
+
assert set(cls.attributes_by_name) == {"attribute", "attribute_mfg"}
|
|
1450
|
+
assert cls.find_attribute("attribute") is cls.AttributeDefs.attribute
|
|
1451
|
+
assert (
|
|
1452
|
+
cls.find_attribute(0x0001, manufacturer_code=0x1234)
|
|
1453
|
+
is cls.AttributeDefs.attribute_mfg
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def test_zcl_cluster_subclass_keeps_same_id_commands():
|
|
1458
|
+
"""Subclassing a cluster keeps two command definitions sharing an ID."""
|
|
1459
|
+
|
|
1460
|
+
class TestCluster(zcl.Cluster):
|
|
1461
|
+
cluster_id = 0xABCD
|
|
1462
|
+
ep_attribute = "test_cluster"
|
|
1463
|
+
_skip_registry = True
|
|
1464
|
+
|
|
1465
|
+
class ServerCommandDefs(zcl.BaseCommandDefs):
|
|
1466
|
+
command = foundation.ZCLCommandDef(id=0x00, schema={"param1": t.uint8_t})
|
|
1467
|
+
command_mfg = foundation.ZCLCommandDef(
|
|
1468
|
+
id=0x00,
|
|
1469
|
+
schema={"param1": t.uint8_t},
|
|
1470
|
+
manufacturer_code=0x1234,
|
|
1471
|
+
)
|
|
1472
|
+
|
|
1473
|
+
class ClientCommandDefs(zcl.BaseCommandDefs):
|
|
1474
|
+
response = foundation.ZCLCommandDef(id=0x01, schema={})
|
|
1475
|
+
response_mfg = foundation.ZCLCommandDef(
|
|
1476
|
+
id=0x01,
|
|
1477
|
+
schema={},
|
|
1478
|
+
manufacturer_code=0x1234,
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
class TestClusterSubclass(TestCluster):
|
|
1482
|
+
pass
|
|
1483
|
+
|
|
1484
|
+
class TestClusterSubSubclass(TestClusterSubclass):
|
|
1485
|
+
pass
|
|
1486
|
+
|
|
1487
|
+
for cls in (TestCluster, TestClusterSubclass, TestClusterSubSubclass):
|
|
1488
|
+
assert set(cls.commands_by_name) == {
|
|
1489
|
+
"command",
|
|
1490
|
+
"command_mfg",
|
|
1491
|
+
"response",
|
|
1492
|
+
"response_mfg",
|
|
1493
|
+
}
|
|
1494
|
+
assert {cmd.name for cmd in cls.ServerCommandDefs} == {
|
|
1495
|
+
"command",
|
|
1496
|
+
"command_mfg",
|
|
1497
|
+
}
|
|
1498
|
+
assert {cmd.name for cmd in cls.ClientCommandDefs} == {
|
|
1499
|
+
"response",
|
|
1500
|
+
"response_mfg",
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def test_zcl_cluster_subclass_old_style_definitions():
|
|
1505
|
+
"""Subclasses of clusters with old-style definitions inherit the rebuilt defs."""
|
|
1506
|
+
|
|
1507
|
+
class TestCluster(zcl.Cluster):
|
|
1508
|
+
cluster_id = 0xABCD
|
|
1509
|
+
ep_attribute = "test_cluster"
|
|
1510
|
+
_skip_registry = True
|
|
1511
|
+
|
|
1512
|
+
attributes = {
|
|
1513
|
+
0x1234: ("attribute", t.uint8_t),
|
|
1514
|
+
0x1235: ("attribute2", t.uint32_t, True),
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
server_commands = {
|
|
1518
|
+
0x00: ("server_command", (t.uint8_t,), True),
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
client_commands = {
|
|
1522
|
+
0x01: ("client_command", (t.uint8_t,), False),
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
class TestClusterSubclass(TestCluster):
|
|
1526
|
+
pass
|
|
1527
|
+
|
|
1528
|
+
assert set(TestClusterSubclass.attributes_by_name) == {"attribute", "attribute2"}
|
|
1529
|
+
assert TestClusterSubclass.AttributeDefs.attribute.id == 0x1234
|
|
1530
|
+
assert TestClusterSubclass.AttributeDefs.attribute2.is_manufacturer_specific
|
|
1531
|
+
assert set(TestClusterSubclass.commands_by_name) == {
|
|
1532
|
+
"server_command",
|
|
1533
|
+
"client_command",
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
# An explicit empty old-style dict does not wipe the inherited definitions
|
|
1537
|
+
class TestClusterEmptyOverride(TestCluster):
|
|
1538
|
+
attributes = {}
|
|
1539
|
+
server_commands = {}
|
|
1540
|
+
client_commands = {}
|
|
1541
|
+
|
|
1542
|
+
assert set(TestClusterEmptyOverride.attributes_by_name) == {
|
|
1543
|
+
"attribute",
|
|
1544
|
+
"attribute2",
|
|
1545
|
+
}
|
|
1546
|
+
assert set(TestClusterEmptyOverride.commands_by_name) == {
|
|
1547
|
+
"server_command",
|
|
1548
|
+
"client_command",
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
|
|
1426
1552
|
async def test_zcl_cluster_definition_invalid_name():
|
|
1427
1553
|
# This is fine
|
|
1428
1554
|
class TestCluster(zcl.Cluster):
|
|
@@ -15,7 +15,7 @@ import os
|
|
|
15
15
|
import random
|
|
16
16
|
import time
|
|
17
17
|
import typing
|
|
18
|
-
from typing import Any, ParamSpec, TypeVar
|
|
18
|
+
from typing import Any, ClassVar, ParamSpec, TypeVar
|
|
19
19
|
import warnings
|
|
20
20
|
|
|
21
21
|
import zigpy.appdb
|
|
@@ -53,10 +53,19 @@ _P = ParamSpec("_P")
|
|
|
53
53
|
CHANNEL_CHANGE_BROADCAST_DELAY_S = 1.0
|
|
54
54
|
CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S = 1.0
|
|
55
55
|
|
|
56
|
+
# Entry point group used by radio libraries to advertise their controller class:
|
|
57
|
+
# the entry point name is the radio type and its value is the controller class
|
|
58
|
+
RADIO_ENTRY_POINT_GROUP = "zigpy.radio"
|
|
59
|
+
|
|
56
60
|
|
|
57
61
|
class ControllerApplication(zigpy.util.ListenableMixin, abc.ABC):
|
|
58
62
|
SCHEMA = conf.CONFIG_SCHEMA
|
|
59
63
|
|
|
64
|
+
# User-facing metadata, set by radio libraries advertising themselves via the
|
|
65
|
+
# `zigpy.radio` entry point group
|
|
66
|
+
DISPLAY_NAME: ClassVar[str | None] = None
|
|
67
|
+
DESCRIPTION: ClassVar[str | None] = None
|
|
68
|
+
|
|
60
69
|
_watchdog_period: int = 30
|
|
61
70
|
_probe_configs: list[dict[str, Any]] = []
|
|
62
71
|
|
|
@@ -461,6 +461,10 @@ class OTA:
|
|
|
461
461
|
|
|
462
462
|
def register_provider(self, provider: zigpy.ota.providers.BaseOtaProvider) -> None:
|
|
463
463
|
"""Register a new OTA provider."""
|
|
464
|
+
if provider in self._providers:
|
|
465
|
+
_LOGGER.warning("Ignoring duplicate OTA provider: %s", provider)
|
|
466
|
+
return
|
|
467
|
+
|
|
464
468
|
_LOGGER.debug("Registering new OTA provider: %s", provider)
|
|
465
469
|
self._providers.append(provider)
|
|
466
470
|
|
|
@@ -247,13 +247,17 @@ class BaseOtaProvider:
|
|
|
247
247
|
raise NotImplementedError
|
|
248
248
|
|
|
249
249
|
def __eq__(self, other: object) -> bool:
|
|
250
|
-
if not isinstance(other,
|
|
250
|
+
if not isinstance(other, BaseOtaProvider):
|
|
251
251
|
return NotImplemented
|
|
252
252
|
|
|
253
|
+
# Providers of a different concrete type are never equal
|
|
254
|
+
if type(self) is not type(other):
|
|
255
|
+
return False
|
|
256
|
+
|
|
253
257
|
return self.url == other.url and self.manufacturer_ids == other.manufacturer_ids
|
|
254
258
|
|
|
255
259
|
def __hash__(self) -> int:
|
|
256
|
-
return hash((self.url, self.manufacturer_ids))
|
|
260
|
+
return hash((type(self), self.url, self.manufacturer_ids))
|
|
257
261
|
|
|
258
262
|
def __repr__(self) -> str:
|
|
259
263
|
return f"{self.__class__.__name__}(url={self.url!r}, manufacturer_ids={self.manufacturer_ids!r})"
|
|
@@ -372,21 +376,36 @@ class Ledvance(BaseOtaProvider):
|
|
|
372
376
|
)
|
|
373
377
|
|
|
374
378
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
class Salus(BaseOtaProvider):
|
|
378
|
-
NAME = "salus"
|
|
379
|
-
MANUFACTURER_IDS = (4216, 43981)
|
|
379
|
+
class StubOtaProvider(BaseOtaProvider):
|
|
380
|
+
"""Stub provider to keep existing configurations working."""
|
|
380
381
|
|
|
381
382
|
VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL
|
|
382
383
|
|
|
383
384
|
async def _load_index(
|
|
384
385
|
self, session: aiohttp.ClientSession
|
|
385
386
|
) -> typing.AsyncIterator[BaseOtaImageMetadata]:
|
|
386
|
-
if
|
|
387
|
+
if typing.TYPE_CHECKING:
|
|
387
388
|
yield # pragma: no cover
|
|
388
389
|
|
|
389
390
|
|
|
391
|
+
@register_provider
|
|
392
|
+
class Salus(StubOtaProvider):
|
|
393
|
+
NAME = "salus"
|
|
394
|
+
MANUFACTURER_IDS = (4216, 43981)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@register_provider
|
|
398
|
+
class ThirdReality(StubOtaProvider):
|
|
399
|
+
NAME = "thirdreality"
|
|
400
|
+
MANUFACTURER_IDS = (4659, 4877, 5127)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@register_provider
|
|
404
|
+
class Inovelli(StubOtaProvider):
|
|
405
|
+
NAME = "inovelli"
|
|
406
|
+
MANUFACTURER_IDS = (4655,)
|
|
407
|
+
|
|
408
|
+
|
|
390
409
|
@register_provider
|
|
391
410
|
class Sonoff(BaseOtaProvider):
|
|
392
411
|
NAME = "sonoff"
|
|
@@ -490,7 +509,7 @@ class LocalZigpyProvider(BaseZigpyProvider):
|
|
|
490
509
|
return super().__eq__(other) and self.index_file == other.index_file
|
|
491
510
|
|
|
492
511
|
def __hash__(self) -> int:
|
|
493
|
-
return hash((self.index_file, self.manufacturer_ids))
|
|
512
|
+
return hash((type(self), self.index_file, self.manufacturer_ids))
|
|
494
513
|
|
|
495
514
|
def __repr__(self) -> str:
|
|
496
515
|
return f"{self.__class__.__name__}(index_file={self.index_file!r}, manufacturer_ids={self.manufacturer_ids!r})"
|
|
@@ -583,7 +602,7 @@ class LocalZ2MProvider(BaseZ2MProvider):
|
|
|
583
602
|
return super().__eq__(other) and self.index_file == other.index_file
|
|
584
603
|
|
|
585
604
|
def __hash__(self) -> int:
|
|
586
|
-
return hash((self.index_file, self.manufacturer_ids))
|
|
605
|
+
return hash((type(self), self.index_file, self.manufacturer_ids))
|
|
587
606
|
|
|
588
607
|
def __repr__(self) -> str:
|
|
589
608
|
return f"{self.__class__.__name__}(index_file={self.index_file!r}, manufacturer_ids={self.manufacturer_ids!r})"
|
|
@@ -688,7 +707,7 @@ class ZigpyOtaProvider(BaseZigpyProvider):
|
|
|
688
707
|
return super().__eq__(other) and self.channel == other.channel
|
|
689
708
|
|
|
690
709
|
def __hash__(self) -> int:
|
|
691
|
-
return hash((self.url, self.channel, self.manufacturer_ids))
|
|
710
|
+
return hash((type(self), self.url, self.channel, self.manufacturer_ids))
|
|
692
711
|
|
|
693
712
|
def __repr__(self) -> str:
|
|
694
713
|
return f"{self.__class__.__name__}(url={self.url!r}, channel={self.channel!r}, manufacturer_ids={self.manufacturer_ids!r})"
|
|
@@ -751,7 +770,7 @@ class AdvancedFileProvider(BaseOtaProvider):
|
|
|
751
770
|
return super().__eq__(other) and self.path == other.path
|
|
752
771
|
|
|
753
772
|
def __hash__(self) -> int:
|
|
754
|
-
return hash((self.path, self.manufacturer_ids))
|
|
773
|
+
return hash((type(self), self.path, self.manufacturer_ids))
|
|
755
774
|
|
|
756
775
|
def __repr__(self) -> str:
|
|
757
776
|
return f"{self.__class__.__name__}(path={self.path!r}, manufacturer_ids={self.manufacturer_ids!r})"
|
|
@@ -400,8 +400,11 @@ class Cluster(util.ListenableMixin, util.CatchingTaskMixin, EventBase):
|
|
|
400
400
|
|
|
401
401
|
cls.attributes[attr.id] = attr.replace(id=attr_id)
|
|
402
402
|
|
|
403
|
-
# Create new definitions from
|
|
404
|
-
|
|
403
|
+
# Create new definitions from old-style definitions authored by this class
|
|
404
|
+
# itself: an MRO-inherited `attributes` dict is keyed purely by attribute ID
|
|
405
|
+
# and cannot represent two same-ID attributes (e.g. a standard and a
|
|
406
|
+
# manufacturer-specific one), so rebuilding from it would drop one of them
|
|
407
|
+
if cls.__dict__.get("attributes") and "AttributeDefs" not in cls.__dict__:
|
|
405
408
|
cls.AttributeDefs = types.new_class(
|
|
406
409
|
name="AttributeDefs",
|
|
407
410
|
bases=(BaseAttributeDefs,),
|
|
@@ -410,7 +413,10 @@ class Cluster(util.ListenableMixin, util.CatchingTaskMixin, EventBase):
|
|
|
410
413
|
for attr in cls.attributes.values():
|
|
411
414
|
setattr(cls.AttributeDefs, attr.name, attr)
|
|
412
415
|
|
|
413
|
-
if
|
|
416
|
+
if (
|
|
417
|
+
cls.__dict__.get("server_commands")
|
|
418
|
+
and "ServerCommandDefs" not in cls.__dict__
|
|
419
|
+
):
|
|
414
420
|
cls.ServerCommandDefs = types.new_class(
|
|
415
421
|
name="ServerCommandDefs",
|
|
416
422
|
bases=(BaseCommandDefs,),
|
|
@@ -419,7 +425,10 @@ class Cluster(util.ListenableMixin, util.CatchingTaskMixin, EventBase):
|
|
|
419
425
|
for command in cls.server_commands.values():
|
|
420
426
|
setattr(cls.ServerCommandDefs, command.name, command)
|
|
421
427
|
|
|
422
|
-
if
|
|
428
|
+
if (
|
|
429
|
+
cls.__dict__.get("client_commands")
|
|
430
|
+
and "ClientCommandDefs" not in cls.__dict__
|
|
431
|
+
):
|
|
423
432
|
cls.ClientCommandDefs = types.new_class(
|
|
424
433
|
name="ClientCommandDefs",
|
|
425
434
|
bases=(BaseCommandDefs,),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|