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.
Files changed (125) hide show
  1. {zigpy-1.5.0/zigpy.egg-info → zigpy-1.6.0}/PKG-INFO +1 -1
  2. {zigpy-1.5.0 → zigpy-1.6.0}/pyproject.toml +3 -0
  3. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_quirks.py +28 -0
  4. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zcl.py +126 -0
  5. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/application.py +10 -1
  6. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/__init__.py +4 -0
  7. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/providers.py +31 -12
  8. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/__init__.py +13 -4
  9. {zigpy-1.5.0 → zigpy-1.6.0/zigpy.egg-info}/PKG-INFO +1 -1
  10. {zigpy-1.5.0 → zigpy-1.6.0}/COPYING +0 -0
  11. {zigpy-1.5.0 → zigpy-1.6.0}/LICENSE +0 -0
  12. {zigpy-1.5.0 → zigpy-1.6.0}/README.md +0 -0
  13. {zigpy-1.5.0 → zigpy-1.6.0}/setup.cfg +0 -0
  14. {zigpy-1.5.0 → zigpy-1.6.0}/setup.py +0 -0
  15. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_app_state.py +0 -0
  16. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_appdb.py +0 -0
  17. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_appdb_migration.py +0 -0
  18. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_application.py +0 -0
  19. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_backups.py +0 -0
  20. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_config.py +0 -0
  21. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_datastructures.py +0 -0
  22. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_datastructures_cpython.py +0 -0
  23. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_device.py +0 -0
  24. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_endpoint.py +0 -0
  25. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_event.py +0 -0
  26. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_group.py +0 -0
  27. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_listeners.py +0 -0
  28. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_packet_callbacks.py +0 -0
  29. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_quirks_registry.py +0 -0
  30. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_quirks_v2.py +0 -0
  31. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_serial.py +0 -0
  32. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_struct.py +0 -0
  33. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_topology.py +0 -0
  34. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_types.py +0 -0
  35. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zcl_clusters.py +0 -0
  36. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zcl_foundation.py +0 -0
  37. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zcl_helpers.py +0 -0
  38. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zdo.py +0 -0
  39. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zdo_types.py +0 -0
  40. {zigpy-1.5.0 → zigpy-1.6.0}/tests/test_zigbee_util.py +0 -0
  41. {zigpy-1.5.0 → zigpy-1.6.0}/tools/__init__.py +0 -0
  42. {zigpy-1.5.0 → zigpy-1.6.0}/tools/regenerate_mypy_ignores.py +0 -0
  43. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/__init__.py +0 -0
  44. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb.py +0 -0
  45. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/__init__.py +0 -0
  46. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v0.sql +0 -0
  47. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v1.sql +0 -0
  48. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v10.sql +0 -0
  49. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v11.sql +0 -0
  50. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v12.sql +0 -0
  51. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v13.sql +0 -0
  52. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v14.sql +0 -0
  53. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v15.sql +0 -0
  54. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v2.sql +0 -0
  55. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v3.sql +0 -0
  56. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v4.sql +0 -0
  57. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v5.sql +0 -0
  58. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v6.sql +0 -0
  59. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v7.sql +0 -0
  60. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v8.sql +0 -0
  61. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/appdb_schemas/schema_v9.sql +0 -0
  62. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/backports/__init__.py +0 -0
  63. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/backups.py +0 -0
  64. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/config/__init__.py +0 -0
  65. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/config/defaults.py +0 -0
  66. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/config/validators.py +0 -0
  67. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/const.py +0 -0
  68. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/datastructures.py +0 -0
  69. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/device.py +0 -0
  70. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/endpoint.py +0 -0
  71. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/event/__init__.py +0 -0
  72. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/event/event_base.py +0 -0
  73. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/exceptions.py +0 -0
  74. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/group.py +0 -0
  75. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/listeners.py +0 -0
  76. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/image.py +0 -0
  77. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/json_schemas.py +0 -0
  78. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/manager.py +0 -0
  79. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/ota/validators.py +0 -0
  80. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/profiles/__init__.py +0 -0
  81. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/profiles/zgp.py +0 -0
  82. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/profiles/zha.py +0 -0
  83. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/profiles/zll.py +0 -0
  84. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/py.typed +0 -0
  85. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/__init__.py +0 -0
  86. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/registry.py +0 -0
  87. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/__init__.py +0 -0
  88. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/homeassistant/__init__.py +0 -0
  89. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/homeassistant/binary_sensor.py +0 -0
  90. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/homeassistant/number.py +0 -0
  91. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/quirks/v2/homeassistant/sensor.py +0 -0
  92. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/serial.py +0 -0
  93. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/state.py +0 -0
  94. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/topology.py +0 -0
  95. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/types/__init__.py +0 -0
  96. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/types/basic.py +0 -0
  97. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/types/named.py +0 -0
  98. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/types/struct.py +0 -0
  99. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/typing.py +0 -0
  100. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/util.py +0 -0
  101. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/__init__.py +0 -0
  102. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/closures.py +0 -0
  103. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/general.py +0 -0
  104. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/general_const.py +0 -0
  105. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/greenpower.py +0 -0
  106. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/homeautomation.py +0 -0
  107. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/hvac.py +0 -0
  108. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/lighting.py +0 -0
  109. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/lightlink.py +0 -0
  110. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/manufacturer_specific.py +0 -0
  111. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/measurement.py +0 -0
  112. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/protocol.py +0 -0
  113. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/security.py +0 -0
  114. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/smartenergy.py +0 -0
  115. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/clusters/wwah.py +0 -0
  116. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/foundation.py +0 -0
  117. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zcl/helpers.py +0 -0
  118. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zdo/__init__.py +0 -0
  119. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zdo/types.py +0 -0
  120. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zgp/__init__.py +0 -0
  121. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy/zgp/types.py +0 -0
  122. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy.egg-info/SOURCES.txt +0 -0
  123. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy.egg-info/dependency_links.txt +0 -0
  124. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy.egg-info/requires.txt +0 -0
  125. {zigpy-1.5.0 → zigpy-1.6.0}/zigpy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zigpy
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Library implementing a Zigbee stack
5
5
  Author-email: Russell Cloran <rcloran@gmail.com>
6
6
  License: GPL-3.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, self.__class__):
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
- # stub provider to keep existing configurations working
376
- @register_provider
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 False:
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 the old-style definitions
404
- if cls.attributes and "AttributeDefs" not in cls.__dict__:
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 cls.server_commands and "ServerCommandDefs" not in cls.__dict__:
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 cls.client_commands and "ClientCommandDefs" not in cls.__dict__:
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,),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zigpy
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Library implementing a Zigbee stack
5
5
  Author-email: Russell Cloran <rcloran@gmail.com>
6
6
  License: GPL-3.0
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