zwave-js-server-python 0.35.0__tar.gz → 0.36.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 (79) hide show
  1. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/PKG-INFO +1 -1
  2. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/setup.py +1 -1
  3. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/test/test_main.py +1 -1
  4. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/__init__.py +22 -2
  5. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/multilevel_switch.py +8 -0
  6. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/event.py +1 -1
  7. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/exceptions.py +23 -14
  8. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/association.py +1 -1
  9. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/__init__.py +121 -62
  10. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/data_model.py +2 -2
  11. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/statistics.py +40 -1
  12. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/device_config.py +2 -2
  13. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/driver.py +4 -0
  14. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/endpoint.py +18 -0
  15. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/log_message.py +2 -2
  16. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/node/__init__.py +19 -7
  17. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/node/data_model.py +2 -2
  18. zwave-js-server-python-0.36.0/zwave_js_server/model/node/statistics.py +119 -0
  19. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/notification.py +47 -0
  20. zwave-js-server-python-0.36.0/zwave_js_server/model/statistics.py +77 -0
  21. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/PKG-INFO +1 -1
  22. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/SOURCES.txt +1 -3
  23. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/top_level.txt +0 -1
  24. zwave-js-server-python-0.35.0/scripts/__init__.py +0 -1
  25. zwave-js-server-python-0.35.0/scripts/generate_multilevel_sensor_constants.py +0 -245
  26. zwave-js-server-python-0.35.0/scripts/run_mock_server.py +0 -385
  27. zwave-js-server-python-0.35.0/zwave_js_server/model/node/statistics.py +0 -67
  28. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/LICENSE +0 -0
  29. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/README.md +0 -0
  30. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/setup.cfg +0 -0
  31. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/test/test_client.py +0 -0
  32. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/test/test_dump.py +0 -0
  33. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/test/test_event.py +0 -0
  34. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/test/test_firmware.py +0 -0
  35. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/test/test_version.py +0 -0
  36. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/__init__.py +0 -0
  37. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/__main__.py +0 -0
  38. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/client.py +0 -0
  39. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/__init__.py +0 -0
  40. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/barrier_operator.py +0 -0
  41. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/color_switch.py +0 -0
  42. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/entry_control.py +0 -0
  43. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/humidity_control.py +0 -0
  44. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/lock.py +0 -0
  45. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/meter.py +0 -0
  46. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/multilevel_sensor.py +0 -0
  47. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/notification.py +0 -0
  48. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/power_level.py +0 -0
  49. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/protection.py +0 -0
  50. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/sound_switch.py +0 -0
  51. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/thermostat.py +0 -0
  52. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/wake_up.py +0 -0
  53. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/dump.py +0 -0
  54. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/firmware.py +0 -0
  55. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/__init__.py +0 -0
  56. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/command_class.py +0 -0
  57. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/event_model.py +0 -0
  58. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/inclusion_and_provisioning.py +0 -0
  59. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/device_class.py +0 -0
  60. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/firmware.py +0 -0
  61. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/log_config.py +0 -0
  62. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/node/event_model.py +0 -0
  63. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/node/health_check.py +0 -0
  64. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/utils.py +0 -0
  65. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/value.py +0 -0
  66. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/model/version.py +0 -0
  67. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/util/__init__.py +0 -0
  68. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/util/command_class/__init__.py +0 -0
  69. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/util/command_class/meter.py +0 -0
  70. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/util/command_class/multilevel_sensor.py +0 -0
  71. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/util/helpers.py +0 -0
  72. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/util/lock.py +0 -0
  73. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/util/multicast.py +0 -0
  74. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/util/node.py +0 -0
  75. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server/version.py +0 -0
  76. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/dependency_links.txt +0 -0
  77. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/entry_points.txt +0 -0
  78. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/not-zip-safe +0 -0
  79. {zwave-js-server-python-0.35.0 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/requires.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zwave-js-server-python
3
- Version: 0.35.0
3
+ Version: 0.36.0
4
4
  Summary: Python wrapper for zwave-js-server
5
5
  Home-page: https://github.com/home-assistant-libs/zwave-js-server-python
6
6
  Author: Home Assistant Team
@@ -5,7 +5,7 @@ from setuptools import find_packages, setup
5
5
 
6
6
  PROJECT_DIR = Path(__file__).parent.resolve()
7
7
  README_FILE = PROJECT_DIR / "README.md"
8
- VERSION = "0.35.0"
8
+ VERSION = "0.36.0"
9
9
 
10
10
 
11
11
  setup(
@@ -64,7 +64,7 @@ def test_dump_state(
64
64
  assert captured.out == (
65
65
  "{'type': 'version', 'driverVersion': 'test_driver_version', "
66
66
  "'serverVersion': 'test_server_version', 'homeId': 'test_home_id', "
67
- "'minSchemaVersion': 0, 'maxSchemaVersion': 15}\n"
67
+ "'minSchemaVersion': 0, 'maxSchemaVersion': 16}\n"
68
68
  "{'type': 'result', 'success': True, 'result': {}, 'messageId': 'api-schema-id'}\n"
69
69
  "test_result\n"
70
70
  )
@@ -3,9 +3,9 @@ import sys
3
3
  from enum import Enum, IntEnum
4
4
 
5
5
  # minimal server schema version we can handle
6
- MIN_SERVER_SCHEMA_VERSION = 15
6
+ MIN_SERVER_SCHEMA_VERSION = 16
7
7
  # max server schema version we can handle (and our code is compatible with)
8
- MAX_SERVER_SCHEMA_VERSION = 15
8
+ MAX_SERVER_SCHEMA_VERSION = 16
9
9
 
10
10
  TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED = sys.version_info < (3, 9, 2)
11
11
 
@@ -297,3 +297,23 @@ class RFRegion(IntEnum):
297
297
  KOREA = 33
298
298
  UNKNOWN = 254
299
299
  DEFAULT_EU = 255
300
+
301
+
302
+ class ProtocolDataRate(IntEnum):
303
+ """Enum for all known protocol data rates."""
304
+
305
+ # https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/capabilities/Protocols.ts#L6
306
+
307
+ ZWAVE_9K6 = 1
308
+ ZWAVE_40K = 2
309
+ ZWAVE_100K = 3
310
+ LONG_RANGE_100K = 4
311
+
312
+
313
+ class RssiError(IntEnum):
314
+ """Enum for all known RSSI errors."""
315
+
316
+ # https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/controller/SendDataShared.ts#L79
317
+ NOT_AVAILABLE = 127
318
+ RECEIVER_SATURATED = 126
319
+ NO_SIGNAL_DETECTED = 125
@@ -1,4 +1,5 @@
1
1
  """Constants for the Multilevel Switch CC."""
2
+ from enum import IntEnum
2
3
 
3
4
  COVER_OPEN_PROPERTY = "Open"
4
5
  COVER_UP_PROPERTY = "Up"
@@ -6,3 +7,10 @@ COVER_ON_PROPERTY = "On"
6
7
  COVER_CLOSE_PROPERTY = "Close"
7
8
  COVER_DOWN_PROPERTY = "Down"
8
9
  COVER_OFF_PROPERTY = "Off"
10
+
11
+
12
+ class MultilevelSwitchCommand(IntEnum):
13
+ """Enum for known multilevel switch notifications."""
14
+
15
+ START_LEVEL_CHANGE = 4
16
+ STOP_LEVEL_CHANGE = 5
@@ -57,7 +57,7 @@ class EventBase:
57
57
 
58
58
  def emit(self, event_name: str, data: dict) -> None:
59
59
  """Run all callbacks for an event."""
60
- for listener in self._listeners.get(event_name, []):
60
+ for listener in self._listeners.get(event_name, []).copy():
61
61
  listener(data)
62
62
 
63
63
  def _handle_event_protocol(self, event: Event) -> None:
@@ -1,10 +1,10 @@
1
1
  """Exceptions for zwave-js-server."""
2
- from typing import TYPE_CHECKING, Optional
2
+ from typing import TYPE_CHECKING, List, Optional
3
3
 
4
- from .const import CommandClass
4
+ from .const import RssiError
5
5
 
6
6
  if TYPE_CHECKING:
7
- from .event import Event
7
+ from .const import CommandClass
8
8
  from .model.value import Value
9
9
 
10
10
 
@@ -129,7 +129,7 @@ class BulkSetConfigParameterFailed(BaseZwaveJSServerError):
129
129
  class InvalidCommandClass(BaseZwaveJSServerError):
130
130
  """Exception raised when Zwave Value has an invalid command class."""
131
131
 
132
- def __init__(self, value: "Value", command_class: CommandClass) -> None:
132
+ def __init__(self, value: "Value", command_class: "CommandClass") -> None:
133
133
  """Initialize an invalid Command Class error."""
134
134
  self.value = value
135
135
  self.command_class = command_class
@@ -159,14 +159,23 @@ class UnknownValueData(BaseZwaveJSServerError):
159
159
  )
160
160
 
161
161
 
162
- class NotificationHasUnsupportedCommandClass(BaseZwaveJSServerError):
163
- """Exception raised when notification is received for an unsupported CC."""
162
+ class RssiErrorReceived(BaseZwaveJSServerError):
163
+ """Exception raised when an RSSI error is received."""
164
164
 
165
- def __init__(self, event: "Event", command_class: CommandClass) -> None:
166
- """Initialize an invalid Command Class error."""
167
- self.event_data = event.data
168
- self.command_class = command_class
169
- super().__init__(
170
- "Notification received with unsupported command class "
171
- f"{command_class.name}: {event.data}"
172
- )
165
+ def __init__(self, error: "RssiError") -> None:
166
+ """Initialize an RSSI error."""
167
+ self.error = error
168
+ super().__init__()
169
+
170
+
171
+ class RepeaterRssiErrorReceived(BaseZwaveJSServerError):
172
+ """Exception raised when an RSSI error is received in list of RSSIs."""
173
+
174
+ def __init__(self, rssi_list: List[int]) -> None:
175
+ """Initialize an RSSI error."""
176
+ self.rssi_list = rssi_list
177
+ rssi_errors = [item.value for item in RssiError]
178
+ self.error_list = [
179
+ RssiError(rssi_) if rssi_ in rssi_errors else None for rssi_ in rssi_list
180
+ ]
181
+ super().__init__()
@@ -16,7 +16,7 @@ class AssociationGroup:
16
16
 
17
17
 
18
18
  @dataclass
19
- class Association:
19
+ class AssociationAddress:
20
20
  """Represent a association dict type."""
21
21
 
22
22
  node_id: int
@@ -12,7 +12,7 @@ from ...const import (
12
12
  )
13
13
  from ...event import Event, EventBase
14
14
  from ...util.helpers import convert_base64_to_bytes, convert_bytes_to_base64
15
- from ..association import Association, AssociationGroup
15
+ from ..association import AssociationAddress, AssociationGroup
16
16
  from ..node import Node
17
17
  from .data_model import ControllerDataType
18
18
  from .event_model import CONTROLLER_EVENT_MODEL_MAP
@@ -21,7 +21,7 @@ from .inclusion_and_provisioning import (
21
21
  ProvisioningEntry,
22
22
  QRProvisioningInformation,
23
23
  )
24
- from .statistics import ControllerStatistics
24
+ from .statistics import ControllerLifelineRoutes, ControllerStatistics
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from ...client import Client
@@ -44,6 +44,7 @@ class Controller(EventBase):
44
44
  self.client = client
45
45
  self.nodes: Dict[int, Node] = {}
46
46
  self._heal_network_progress: Optional[Dict[int, str]] = None
47
+ self._statistics = ControllerStatistics()
47
48
  for node_state in state["nodes"]:
48
49
  node = Node(client, node_state)
49
50
  self.nodes[node.node_id] = node
@@ -64,9 +65,9 @@ class Controller(EventBase):
64
65
  return self.home_id == other.home_id
65
66
 
66
67
  @property
67
- def library_version(self) -> Optional[str]:
68
- """Return library_version."""
69
- return self.data.get("libraryVersion")
68
+ def sdk_version(self) -> Optional[str]:
69
+ """Return sdk_version."""
70
+ return self.data.get("sdkVersion")
70
71
 
71
72
  @property
72
73
  def controller_type(self) -> Optional[int]:
@@ -114,9 +115,9 @@ class Controller(EventBase):
114
115
  return self.data.get("isSlave")
115
116
 
116
117
  @property
117
- def serial_api_version(self) -> Optional[str]:
118
- """Return serial_api_version."""
119
- return self.data.get("serialApiVersion")
118
+ def firmware_version(self) -> Optional[str]:
119
+ """Return firmware_version."""
120
+ return self.data.get("firmwareVersion")
120
121
 
121
122
  @property
122
123
  def manufacturer_id(self) -> Optional[int]:
@@ -420,18 +421,21 @@ class Controller(EventBase):
420
421
  return cast(bool, data["failed"])
421
422
 
422
423
  async def async_get_association_groups(
423
- self, node_id: int
424
+ self, source: AssociationAddress
424
425
  ) -> Dict[int, AssociationGroup]:
425
426
  """Send getAssociationGroups command to Controller."""
427
+ source_data = {"nodeId": source.node_id}
428
+ if source.endpoint is not None:
429
+ source_data["endpoint"] = source.endpoint
426
430
  data = await self.client.async_send_command(
427
431
  {
428
432
  "command": "controller.get_association_groups",
429
- "nodeId": node_id,
433
+ **source_data,
430
434
  }
431
435
  )
432
436
  groups = {}
433
437
  for key, group in data["groups"].items():
434
- groups[key] = AssociationGroup(
438
+ groups[int(key)] = AssociationGroup(
435
439
  max_nodes=group["maxNodes"],
436
440
  is_lifeline=group["isLifeline"],
437
441
  multi_channel=group["multiChannel"],
@@ -441,84 +445,125 @@ class Controller(EventBase):
441
445
  )
442
446
  return groups
443
447
 
444
- async def async_get_associations(self, node_id: int) -> Dict[int, Association]:
448
+ async def async_get_associations(
449
+ self, source: AssociationAddress
450
+ ) -> Dict[int, List[AssociationAddress]]:
445
451
  """Send getAssociations command to Controller."""
452
+ source_data = {"nodeId": source.node_id}
453
+ if source.endpoint is not None:
454
+ source_data["endpoint"] = source.endpoint
446
455
  data = await self.client.async_send_command(
447
456
  {
448
457
  "command": "controller.get_associations",
449
- "nodeId": node_id,
458
+ **source_data,
450
459
  }
451
460
  )
452
461
  associations = {}
453
- for key, association in data["associations"].items():
454
- associations[key] = Association(
455
- node_id=association["nodeId"], endpoint=association.get("endpoint")
456
- )
462
+ for key, association_addresses in data["associations"].items():
463
+ associations[int(key)] = [
464
+ AssociationAddress(
465
+ node_id=association_address["nodeId"],
466
+ endpoint=association_address.get("endpoint"),
467
+ )
468
+ for association_address in association_addresses
469
+ ]
457
470
  return associations
458
471
 
459
472
  async def async_is_association_allowed(
460
- self, node_id: int, group: int, association: Association
473
+ self, source: AssociationAddress, group: int, association: AssociationAddress
461
474
  ) -> bool:
462
475
  """Send isAssociationAllowed command to Controller."""
476
+ source_data = {"nodeId": source.node_id}
477
+ if source.endpoint is not None:
478
+ source_data["endpoint"] = source.endpoint
479
+
480
+ association_data = {"nodeId": association.node_id}
481
+ if association.endpoint is not None:
482
+ association_data["endpoint"] = association.endpoint
463
483
  data = await self.client.async_send_command(
464
484
  {
465
485
  "command": "controller.is_association_allowed",
466
- "nodeId": node_id,
486
+ **source_data,
467
487
  "group": group,
468
- "association": {
469
- "nodeId": association.node_id,
470
- "endpoint": association.endpoint,
471
- },
488
+ "association": association_data,
472
489
  }
473
490
  )
474
491
  return cast(bool, data["allowed"])
475
492
 
476
493
  async def async_add_associations(
477
- self, node_id: int, group: int, associations: List[Association]
494
+ self,
495
+ source: AssociationAddress,
496
+ group: int,
497
+ associations: List[AssociationAddress],
498
+ wait_for_result: bool = False,
478
499
  ) -> None:
479
500
  """Send addAssociations command to Controller."""
480
- await self.client.async_send_command(
481
- {
482
- "command": "controller.add_associations",
483
- "nodeId": node_id,
484
- "group": group,
485
- "associations": [
486
- {
487
- "nodeId": association.node_id,
488
- "endpoint": association.endpoint,
489
- }
490
- for association in associations
491
- ],
492
- }
493
- )
501
+ source_data = {"nodeId": source.node_id}
502
+ if source.endpoint is not None:
503
+ source_data["endpoint"] = source.endpoint
504
+
505
+ associations_data = []
506
+ for association in associations:
507
+ association_data = {"nodeId": association.node_id}
508
+ if association.endpoint is not None:
509
+ association_data["endpoint"] = association.endpoint
510
+ associations_data.append(association_data)
511
+
512
+ cmd = {
513
+ "command": "controller.add_associations",
514
+ **source_data,
515
+ "group": group,
516
+ "associations": associations_data,
517
+ }
518
+ if wait_for_result:
519
+ await self.client.async_send_command(cmd)
520
+ else:
521
+ await self.client.async_send_command_no_wait(cmd)
494
522
 
495
523
  async def async_remove_associations(
496
- self, node_id: int, group: int, associations: List[Association]
524
+ self,
525
+ source: AssociationAddress,
526
+ group: int,
527
+ associations: List[AssociationAddress],
528
+ wait_for_result: bool = False,
497
529
  ) -> None:
498
530
  """Send removeAssociations command to Controller."""
499
- await self.client.async_send_command(
500
- {
501
- "command": "controller.remove_associations",
502
- "nodeId": node_id,
503
- "group": group,
504
- "associations": [
505
- {
506
- "nodeId": association.node_id,
507
- "endpoint": association.endpoint,
508
- }
509
- for association in associations
510
- ],
511
- }
512
- )
531
+ source_data = {"nodeId": source.node_id}
532
+ if source.endpoint is not None:
533
+ source_data["endpoint"] = source.endpoint
534
+
535
+ associations_data = []
536
+ for association in associations:
537
+ association_data = {"nodeId": association.node_id}
538
+ if association.endpoint is not None:
539
+ association_data["endpoint"] = association.endpoint
540
+ associations_data.append(association_data)
541
+
542
+ cmd = {
543
+ "command": "controller.remove_associations",
544
+ **source_data,
545
+ "group": group,
546
+ "associations": associations_data,
547
+ }
548
+ if wait_for_result:
549
+ await self.client.async_send_command(cmd)
550
+ else:
551
+ await self.client.async_send_command_no_wait(cmd)
513
552
 
514
- async def async_remove_node_from_all_associations(self, node_id: int) -> None:
553
+ async def async_remove_node_from_all_associations(
554
+ self,
555
+ node_id: int,
556
+ wait_for_result: bool = False,
557
+ ) -> None:
515
558
  """Send removeNodeFromAllAssociations command to Controller."""
516
- await self.client.async_send_command(
517
- {
518
- "command": "controller.remove_node_from_all_associations",
519
- "nodeId": node_id,
520
- }
521
- )
559
+ cmd = {
560
+ "command": "controller.remove_node_from_all_associations",
561
+ "nodeId": node_id,
562
+ }
563
+ if wait_for_result:
564
+ await self.client.async_send_command(cmd)
565
+ else:
566
+ await self.client.async_send_command_no_wait(cmd)
522
567
 
523
568
  async def async_get_node_neighbors(self, node_id: int) -> List[int]:
524
569
  """Send getNodeNeighbors command to Controller to get node's neighbors."""
@@ -629,6 +674,19 @@ class Controller(EventBase):
629
674
  )
630
675
  return cast(bool, data["success"])
631
676
 
677
+ async def async_get_known_lifeline_routes(
678
+ self,
679
+ ) -> Dict[Node, ControllerLifelineRoutes]:
680
+ """Send getKnownLifelineRoutes command to Controller."""
681
+ data = await self.client.async_send_command(
682
+ {"command": "controller.get_known_lifeline_routes"}, require_schema=16
683
+ )
684
+
685
+ return {
686
+ self.nodes[node_id]: ControllerLifelineRoutes(self.client, lifeline_routes)
687
+ for node_id, lifeline_routes in data["routes"].items()
688
+ }
689
+
632
690
  def receive_event(self, event: Event) -> None:
633
691
  """Receive an event."""
634
692
  if event.data["source"] == "node":
@@ -695,8 +753,9 @@ class Controller(EventBase):
695
753
 
696
754
  def handle_statistics_updated(self, event: Event) -> None:
697
755
  """Process a statistics updated event."""
698
- self._statistics.data.update(event.data["statistics"])
699
- event.data["statistics_updated"] = self.statistics
756
+ self._statistics = event.data["statistics_updated"] = ControllerStatistics(
757
+ event.data["statistics"]
758
+ )
700
759
 
701
760
  def handle_grant_security_classes(self, event: Event) -> None:
702
761
  """Process a grant security classes event."""
@@ -13,7 +13,7 @@ else:
13
13
  class ControllerDataType(TypedDict, total=False):
14
14
  """Represent a controller data dict type."""
15
15
 
16
- libraryVersion: str
16
+ sdkVersion: str
17
17
  type: int
18
18
  homeId: int
19
19
  ownNodeId: int
@@ -23,7 +23,7 @@ class ControllerDataType(TypedDict, total=False):
23
23
  wasRealPrimary: bool
24
24
  isStaticUpdateController: bool
25
25
  isSlave: bool
26
- serialApiVersion: str
26
+ firmwareVersion: str
27
27
  manufacturerId: int
28
28
  productType: int
29
29
  productId: int
@@ -1,13 +1,52 @@
1
1
  """Provide a model for the Z-Wave JS controller's statistics."""
2
- from typing import Optional
2
+ from dataclasses import dataclass
3
+ from typing import TYPE_CHECKING, Optional
3
4
 
4
5
  from ...const import TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED
6
+ from ..statistics import RouteStatistics, RouteStatisticsDataType
5
7
 
6
8
  if TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED:
7
9
  from typing_extensions import TypedDict
8
10
  else:
9
11
  from typing import TypedDict
10
12
 
13
+ if TYPE_CHECKING:
14
+ from ...client import Client
15
+
16
+
17
+ class ControllerLifelineRoutesDataType(TypedDict):
18
+ """Represent a controller lifeline routes data dict type."""
19
+
20
+ lwr: RouteStatisticsDataType
21
+ nlwr: RouteStatisticsDataType
22
+
23
+
24
+ @dataclass
25
+ class ControllerLifelineRoutes:
26
+ """Represent controller lifeline routes."""
27
+
28
+ def __init__(
29
+ self, client: "Client", data: ControllerLifelineRoutesDataType
30
+ ) -> None:
31
+ """Initialize controller lifeline routes."""
32
+ self.data = data
33
+ self._lwr = None
34
+ if lwr := self.data.get("lwr"):
35
+ self._lwr = RouteStatistics(client, lwr)
36
+ self._nlwr = None
37
+ if nlwr := self.data.get("nlwr"):
38
+ self._nlwr = RouteStatistics(client, nlwr)
39
+
40
+ @property
41
+ def lwr(self) -> Optional[RouteStatistics]:
42
+ """Return the last working route from the controller to this node."""
43
+ return self._lwr
44
+
45
+ @property
46
+ def nlwr(self) -> Optional[RouteStatistics]:
47
+ """Return the next to last working route from the controller to this node."""
48
+ return self._nlwr
49
+
11
50
 
12
51
  class ControllerStatisticsDataType(TypedDict):
13
52
  """Represent a controller statistics data dict type."""
@@ -3,7 +3,7 @@ Model for a Zwave Node's device config.
3
3
 
4
4
  https://zwave-js.github.io/node-zwave-js/#/api/node?id=deviceconfig
5
5
  """
6
- from typing import Dict, List, Literal, Optional, Union
6
+ from typing import Any, Dict, List, Literal, Optional, Union
7
7
 
8
8
  from ..const import TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED
9
9
 
@@ -138,7 +138,7 @@ class DeviceConfigDataType(TypedDict, total=False):
138
138
  paramInformation: Dict[str, dict]
139
139
  supportsZWavePlus: bool
140
140
  proprietary: dict
141
- compat: Dict[str, dict]
141
+ compat: Dict[str, Any]
142
142
  metadata: DeviceMetadataDataType
143
143
  isEmbedded: bool
144
144
 
@@ -161,6 +161,10 @@ class Driver(EventBase):
161
161
  "set_preferred_scales", scales=scales, require_schema=6
162
162
  )
163
163
 
164
+ async def async_enable_error_reporting(self) -> None:
165
+ """Send command to enable Sentry error reporting."""
166
+ await self._async_send_command("enable_error_reporting", require_schema=16)
167
+
164
168
  def handle_logging(self, event: Event) -> None:
165
169
  """Process a driver logging event."""
166
170
  event.data["log_message"] = LogMessage(cast(LogMessageDataType, event.data))
@@ -49,6 +49,24 @@ class Endpoint(EventBase):
49
49
  self.values: Dict[str, Union[ConfigurationValue, Value]] = {}
50
50
  self.update(data, values)
51
51
 
52
+ def __repr__(self) -> str:
53
+ """Return the representation."""
54
+ return f"{type(self).__name__}(node_id={self.node_id}, index={self.index})"
55
+
56
+ def __hash__(self) -> int:
57
+ """Return the hash."""
58
+ return hash((self.client.driver, self.node_id, self.index))
59
+
60
+ def __eq__(self, other: object) -> bool:
61
+ """Return whether this instance equals another."""
62
+ if not isinstance(other, Endpoint):
63
+ return False
64
+ return (
65
+ self.client.driver == other.client.driver
66
+ and self.node_id == other.node_id
67
+ and self.index == other.index
68
+ )
69
+
52
70
  @property
53
71
  def node_id(self) -> int:
54
72
  """Return node ID property."""
@@ -13,7 +13,7 @@ class LogMessageContextDataType(TypedDict, total=False):
13
13
  """Represent a log message context data dict type."""
14
14
 
15
15
  source: Literal["config", "serial", "controller", "driver"] # required
16
- type: Literal["value", "node"]
16
+ type: Literal["controller", "value", "node"]
17
17
  nodeId: int
18
18
  header: str
19
19
  direction: Literal["inbound", "outbound", "none"]
@@ -38,7 +38,7 @@ class LogMessageContext:
38
38
  return self.data["source"]
39
39
 
40
40
  @property
41
- def type(self) -> Optional[Literal["value", "node"]]:
41
+ def type(self) -> Optional[Literal["controller", "value", "node"]]:
42
42
  """Return the object type for the log message if applicable."""
43
43
  return self.data.get("type")
44
44