zwave-js-server-python 0.35.2__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 (76) hide show
  1. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/PKG-INFO +1 -1
  2. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/setup.py +1 -1
  3. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/test/test_main.py +1 -1
  4. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/__init__.py +22 -2
  5. {zwave-js-server-python-0.35.2 → 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.2 → zwave-js-server-python-0.36.0}/zwave_js_server/exceptions.py +23 -14
  7. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/association.py +1 -1
  8. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/__init__.py +121 -62
  9. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/data_model.py +2 -2
  10. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/statistics.py +40 -1
  11. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/driver.py +4 -0
  12. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/endpoint.py +18 -0
  13. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/log_message.py +2 -2
  14. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/node/__init__.py +16 -6
  15. zwave-js-server-python-0.36.0/zwave_js_server/model/node/statistics.py +119 -0
  16. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/notification.py +47 -0
  17. zwave-js-server-python-0.36.0/zwave_js_server/model/statistics.py +77 -0
  18. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/PKG-INFO +1 -1
  19. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/SOURCES.txt +1 -0
  20. zwave-js-server-python-0.35.2/zwave_js_server/model/node/statistics.py +0 -67
  21. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/LICENSE +0 -0
  22. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/README.md +0 -0
  23. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/setup.cfg +0 -0
  24. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/test/test_client.py +0 -0
  25. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/test/test_dump.py +0 -0
  26. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/test/test_event.py +0 -0
  27. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/test/test_firmware.py +0 -0
  28. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/test/test_version.py +0 -0
  29. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/__init__.py +0 -0
  30. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/__main__.py +0 -0
  31. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/client.py +0 -0
  32. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/__init__.py +0 -0
  33. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/barrier_operator.py +0 -0
  34. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/color_switch.py +0 -0
  35. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/entry_control.py +0 -0
  36. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/humidity_control.py +0 -0
  37. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/lock.py +0 -0
  38. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/meter.py +0 -0
  39. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/multilevel_sensor.py +0 -0
  40. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/notification.py +0 -0
  41. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/power_level.py +0 -0
  42. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/protection.py +0 -0
  43. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/sound_switch.py +0 -0
  44. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/thermostat.py +0 -0
  45. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/const/command_class/wake_up.py +0 -0
  46. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/dump.py +0 -0
  47. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/event.py +0 -0
  48. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/firmware.py +0 -0
  49. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/__init__.py +0 -0
  50. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/command_class.py +0 -0
  51. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/event_model.py +0 -0
  52. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/controller/inclusion_and_provisioning.py +0 -0
  53. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/device_class.py +0 -0
  54. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/device_config.py +0 -0
  55. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/firmware.py +0 -0
  56. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/log_config.py +0 -0
  57. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/node/data_model.py +0 -0
  58. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/node/event_model.py +0 -0
  59. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/node/health_check.py +0 -0
  60. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/utils.py +0 -0
  61. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/value.py +0 -0
  62. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/model/version.py +0 -0
  63. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/util/__init__.py +0 -0
  64. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/util/command_class/__init__.py +0 -0
  65. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/util/command_class/meter.py +0 -0
  66. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/util/command_class/multilevel_sensor.py +0 -0
  67. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/util/helpers.py +0 -0
  68. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/util/lock.py +0 -0
  69. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/util/multicast.py +0 -0
  70. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/util/node.py +0 -0
  71. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server/version.py +0 -0
  72. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/dependency_links.txt +0 -0
  73. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/entry_points.txt +0 -0
  74. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/not-zip-safe +0 -0
  75. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/requires.txt +0 -0
  76. {zwave-js-server-python-0.35.2 → zwave-js-server-python-0.36.0}/zwave_js_server_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zwave-js-server-python
3
- Version: 0.35.2
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.2"
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
@@ -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."""
@@ -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
 
@@ -1,4 +1,5 @@
1
1
  """Provide a model for the Z-Wave JS node."""
2
+ import logging
2
3
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
3
4
 
4
5
  from ...const import (
@@ -12,7 +13,6 @@ from ...event import Event, EventBase
12
13
  from ...exceptions import (
13
14
  FailedCommand,
14
15
  NotFoundError,
15
- NotificationHasUnsupportedCommandClass,
16
16
  UnparseableValue,
17
17
  UnwriteableValue,
18
18
  )
@@ -33,6 +33,8 @@ from ..notification import (
33
33
  NotificationNotificationDataType,
34
34
  PowerLevelNotification,
35
35
  PowerLevelNotificationDataType,
36
+ MultilevelSwitchNotification,
37
+ MultilevelSwitchNotificationDataType,
36
38
  )
37
39
  from ..value import (
38
40
  ConfigurationValue,
@@ -58,6 +60,9 @@ if TYPE_CHECKING:
58
60
  from ...client import Client
59
61
 
60
62
 
63
+ _LOGGER = logging.getLogger(__package__)
64
+
65
+
61
66
  class Node(EventBase):
62
67
  """Represent a Z-Wave JS node."""
63
68
 
@@ -67,7 +72,7 @@ class Node(EventBase):
67
72
  self.client = client
68
73
  self.data: NodeDataType = {}
69
74
  self._device_config: DeviceConfig = DeviceConfig({})
70
- self._statistics: NodeStatistics = NodeStatistics()
75
+ self._statistics: NodeStatistics = NodeStatistics(client)
71
76
  self._firmware_update_progress: Optional[FirmwareUpdateProgress] = None
72
77
  self.values: Dict[str, Union[ConfigurationValue, Value]] = {}
73
78
  self.endpoints: Dict[int, Endpoint] = {}
@@ -302,7 +307,7 @@ class Node(EventBase):
302
307
  """Update the internal state data."""
303
308
  self.data = data
304
309
  self._device_config = DeviceConfig(self.data.get("deviceConfig", {}))
305
- self._statistics = NodeStatistics(self.data.get("statistics"))
310
+ self._statistics = NodeStatistics(self.client, self.data.get("statistics"))
306
311
 
307
312
  # Remove stale values
308
313
  value_ids = (_get_value_id_from_dict(self, val) for val in data["values"])
@@ -780,6 +785,10 @@ class Node(EventBase):
780
785
  event.data["notification"] = NotificationNotification(
781
786
  self, cast(NotificationNotificationDataType, event.data)
782
787
  )
788
+ elif command_class == CommandClass.SWITCH_MULTILEVEL:
789
+ event.data["notification"] = MultilevelSwitchNotification(
790
+ self, cast(MultilevelSwitchNotificationDataType, event.data)
791
+ )
783
792
  elif command_class == CommandClass.ENTRY_CONTROL:
784
793
  event.data["notification"] = EntryControlNotification(
785
794
  self, cast(EntryControlNotificationDataType, event.data)
@@ -789,7 +798,7 @@ class Node(EventBase):
789
798
  self, cast(PowerLevelNotificationDataType, event.data)
790
799
  )
791
800
  else:
792
- raise NotificationHasUnsupportedCommandClass(event, command_class)
801
+ _LOGGER.info("Unhandled notification command class: %s", command_class.name)
793
802
 
794
803
  def handle_firmware_update_progress(self, event: Event) -> None:
795
804
  """Process a node firmware update progress event."""
@@ -808,5 +817,6 @@ class Node(EventBase):
808
817
 
809
818
  def handle_statistics_updated(self, event: Event) -> None:
810
819
  """Process a statistics updated event."""
811
- self._statistics.data.update(event.data["statistics"])
812
- event.data["statistics_updated"] = self.statistics
820
+ event.data["statistics_updated"] = self._statistics = NodeStatistics(
821
+ self.client, event.data["statistics"]
822
+ )
@@ -0,0 +1,119 @@
1
+ """Provide a model for the Z-Wave JS node's statistics."""
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ from zwave_js_server.exceptions import RssiErrorReceived
5
+
6
+ from ...const import TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED, RssiError
7
+ from ..statistics import RouteStatistics, RouteStatisticsDataType
8
+
9
+ if TYPE_CHECKING:
10
+ from ...client import Client
11
+
12
+ if TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED:
13
+ from typing_extensions import TypedDict
14
+ else:
15
+ from typing import TypedDict
16
+
17
+
18
+ class NodeStatisticsDataType(TypedDict, total=False):
19
+ """Represent a node statistics data dict type."""
20
+
21
+ # https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/node/NodeStatistics.ts#L21-L33
22
+ commandsTX: int
23
+ commandsRX: int
24
+ commandsDroppedTX: int
25
+ commandsDroppedRX: int
26
+ timeoutResponse: int
27
+ rtt: int
28
+ rssi: int
29
+ lwr: RouteStatisticsDataType
30
+ nlwr: RouteStatisticsDataType
31
+
32
+
33
+ class NodeStatistics:
34
+ """Represent a node statistics update."""
35
+
36
+ def __init__(
37
+ self, client: "Client", data: Optional[NodeStatisticsDataType] = None
38
+ ) -> None:
39
+ """Initialize node statistics."""
40
+ self.data = data or NodeStatisticsDataType(
41
+ commandsDroppedRX=0,
42
+ commandsDroppedTX=0,
43
+ commandsRX=0,
44
+ commandsTX=0,
45
+ timeoutResponse=0,
46
+ )
47
+ self.client = client
48
+ self._lwr = None
49
+ if lwr := self.data.get("lwr"):
50
+ self._lwr = RouteStatistics(client, lwr)
51
+ self._nlwr = None
52
+ if nlwr := self.data.get("nlwr"):
53
+ self._nlwr = RouteStatistics(client, nlwr)
54
+
55
+ @property
56
+ def commands_tx(self) -> int:
57
+ """Return number of commands successfully sent to node."""
58
+ return self.data["commandsTX"]
59
+
60
+ @property
61
+ def commands_rx(self) -> int:
62
+ """
63
+ Return number of commands received from node.
64
+
65
+ Includes responses to sent commands.
66
+ """
67
+ return self.data["commandsRX"]
68
+
69
+ @property
70
+ def commands_dropped_rx(self) -> int:
71
+ """Return number of commands from node that were dropped by host."""
72
+ return self.data["commandsDroppedRX"]
73
+
74
+ @property
75
+ def commands_dropped_tx(self) -> int:
76
+ """
77
+ Return number of outgoing commands that were dropped.
78
+
79
+ These commands could not be sent.
80
+ """
81
+ return self.data["commandsDroppedTX"]
82
+
83
+ @property
84
+ def timeout_response(self) -> int:
85
+ """Return number of Get-type cmds where node's response didn't come in time."""
86
+ return self.data["timeoutResponse"]
87
+
88
+ @property
89
+ def rtt(self) -> Optional[int]:
90
+ """
91
+ Return average round trip time (RTT) to this node in milliseconds.
92
+
93
+ Consecutive measurements are combined using an exponential moving average.
94
+ """
95
+ return self.data.get("rtt")
96
+
97
+ @property
98
+ def rssi(self) -> Optional[int]:
99
+ """
100
+ Return average RSSI of frames received by this node.
101
+
102
+ Consecutive non-error measurements are combined using an exponential moving
103
+ average.
104
+ """
105
+ if (rssi_ := self.data.get("rssi")) is None:
106
+ return None
107
+ if rssi_ in [item.value for item in RssiError]:
108
+ raise RssiErrorReceived(RssiError(rssi_))
109
+ return rssi_
110
+
111
+ @property
112
+ def lwr(self) -> Optional[RouteStatistics]:
113
+ """Return last working route from the controller to this node."""
114
+ return self._lwr
115
+
116
+ @property
117
+ def nlwr(self) -> Optional[RouteStatistics]:
118
+ """Return next to last working route from the controller to this node."""
119
+ return self._nlwr
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Union
8
8
 
9
9
  from ..const import TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED
10
10
  from ..const.command_class.power_level import PowerLevelTestStatus
11
+ from ..const.command_class.multilevel_switch import MultilevelSwitchCommand
11
12
  from ..util.helpers import parse_buffer
12
13
 
13
14
  if TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED:
@@ -184,3 +185,49 @@ class PowerLevelNotification:
184
185
  def acknowledged_frames(self) -> int:
185
186
  """Return acknowledged frames property."""
186
187
  return self.data["args"]["acknowledgedFrames"]
188
+
189
+
190
+ class MultilevelSwitchNotificationArgsDataType(TypedDict, total=False):
191
+ """Represent args for a Multi Level Switch CC notification event data dict type."""
192
+
193
+ eventType: int # required
194
+ direction: str
195
+
196
+
197
+ class MultilevelSwitchNotificationDataType(BaseNotificationDataType):
198
+ """Represent a Multi Level Switch CC notification event data dict type."""
199
+
200
+ args: MultilevelSwitchNotificationArgsDataType # required
201
+
202
+
203
+ class MultilevelSwitchNotification:
204
+ """Model for a Zwave Node's Multi Level CC notification event."""
205
+
206
+ def __init__(
207
+ self, node: "Node", data: MultilevelSwitchNotificationDataType
208
+ ) -> None:
209
+ """Initialize."""
210
+ self.node = node
211
+ self.data = data
212
+
213
+ @property
214
+ def node_id(self) -> int:
215
+ """Return node ID property."""
216
+ return self.data["nodeId"]
217
+
218
+ @property
219
+ def command_class(self) -> int:
220
+ """Return command class."""
221
+ return self.data["ccId"]
222
+
223
+ @property
224
+ def event_type(self) -> MultilevelSwitchCommand:
225
+ """Return event type property."""
226
+ return MultilevelSwitchCommand(self.data["args"]["eventType"])
227
+
228
+ @property
229
+ def direction(self) -> Optional[str]:
230
+ """Return direction property."""
231
+ if direction := self.data["args"].get("direction"):
232
+ return direction
233
+ return None
@@ -0,0 +1,77 @@
1
+ """Common models for statistics."""
2
+ from dataclasses import dataclass
3
+ from typing import TYPE_CHECKING, List, Optional
4
+
5
+ from zwave_js_server.exceptions import RepeaterRssiErrorReceived, RssiErrorReceived
6
+
7
+ from ..const import TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED, ProtocolDataRate, RssiError
8
+
9
+ if TYPE_CHECKING:
10
+ from ..client import Client
11
+ from .node import Node
12
+
13
+ if TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED:
14
+ from typing_extensions import TypedDict
15
+ else:
16
+ from typing import TypedDict
17
+
18
+
19
+ class RouteStatisticsDataType(TypedDict, total=False):
20
+ """Represent a route statistics data dict type."""
21
+
22
+ protocolDataRate: int
23
+ repeaters: List[int]
24
+ rssi: int
25
+ repeaterRSSI: List[int]
26
+ routeFailedBetween: List[int]
27
+
28
+
29
+ @dataclass
30
+ class RouteStatistics:
31
+ """Represent route statistics."""
32
+
33
+ def __init__(self, client: "Client", data: RouteStatisticsDataType) -> None:
34
+ """Initialize route statistics."""
35
+ self.data = data
36
+ self.client = client
37
+
38
+ @property
39
+ def protocol_data_rate(self) -> ProtocolDataRate:
40
+ """Return protocol data rate."""
41
+ return ProtocolDataRate(self.data["protocolDataRate"])
42
+
43
+ @property
44
+ def repeaters(self) -> List["Node"]:
45
+ """Return repeaters."""
46
+ assert self.client.driver
47
+ return [
48
+ self.client.driver.controller.nodes[node_id]
49
+ for node_id in self.data["repeaters"]
50
+ ]
51
+
52
+ @property
53
+ def rssi(self) -> Optional[int]:
54
+ """Return RSSI."""
55
+ if (rssi := self.data.get("rssi")) is None:
56
+ return None
57
+ if rssi in [item.value for item in RssiError]:
58
+ raise RssiErrorReceived(RssiError(rssi))
59
+ return rssi
60
+
61
+ @property
62
+ def repeater_rssi(self) -> List[int]:
63
+ """Return repeater RSSI."""
64
+ repeater_rssi = self.data.get("repeaterRSSI", [])
65
+ rssi_errors = [item.value for item in RssiError]
66
+ if any(rssi_ in rssi_errors for rssi_ in repeater_rssi):
67
+ raise RepeaterRssiErrorReceived(repeater_rssi)
68
+
69
+ return repeater_rssi
70
+
71
+ @property
72
+ def route_failed_between(self) -> Optional[List["Node"]]:
73
+ """Return route failed between."""
74
+ if (node_ids := self.data.get("routeFailedBetween")) is None:
75
+ return None
76
+ assert self.client.driver
77
+ return [self.client.driver.controller.nodes[node_id] for node_id in node_ids]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zwave-js-server-python
3
- Version: 0.35.2
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
@@ -43,6 +43,7 @@ zwave_js_server/model/firmware.py
43
43
  zwave_js_server/model/log_config.py
44
44
  zwave_js_server/model/log_message.py
45
45
  zwave_js_server/model/notification.py
46
+ zwave_js_server/model/statistics.py
46
47
  zwave_js_server/model/utils.py
47
48
  zwave_js_server/model/value.py
48
49
  zwave_js_server/model/version.py
@@ -1,67 +0,0 @@
1
- """Provide a model for the Z-Wave JS node's statistics."""
2
- from typing import Optional
3
-
4
- from ...const import TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED
5
-
6
- if TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED:
7
- from typing_extensions import TypedDict
8
- else:
9
- from typing import TypedDict
10
-
11
-
12
- class NodeStatisticsDataType(TypedDict):
13
- """Represent a node statistics data dict type."""
14
-
15
- # https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/node/NodeStatistics.ts#L21-L33
16
- commandsTX: int
17
- commandsRX: int
18
- commandsDroppedTX: int
19
- commandsDroppedRX: int
20
- timeoutResponse: int
21
-
22
-
23
- class NodeStatistics:
24
- """Represent a node statistics update."""
25
-
26
- def __init__(self, data: Optional[NodeStatisticsDataType] = None) -> None:
27
- """Initialize node statistics."""
28
- self.data = data or NodeStatisticsDataType(
29
- commandsDroppedRX=0,
30
- commandsDroppedTX=0,
31
- commandsRX=0,
32
- commandsTX=0,
33
- timeoutResponse=0,
34
- )
35
-
36
- @property
37
- def commands_tx(self) -> int:
38
- """Return number of commands successfully sent to node."""
39
- return self.data["commandsTX"]
40
-
41
- @property
42
- def commands_rx(self) -> int:
43
- """
44
- Return number of commands received from node.
45
-
46
- Includes responses to sent commands.
47
- """
48
- return self.data["commandsRX"]
49
-
50
- @property
51
- def commands_dropped_rx(self) -> int:
52
- """Return number of commands from node that were dropped by host."""
53
- return self.data["commandsDroppedRX"]
54
-
55
- @property
56
- def commands_dropped_tx(self) -> int:
57
- """
58
- Return number of outgoing commands that were dropped.
59
-
60
- These commands could not be sent.
61
- """
62
- return self.data["commandsDroppedTX"]
63
-
64
- @property
65
- def timeout_response(self) -> int:
66
- """Return number of Get-type cmds where node's response didn't come in time."""
67
- return self.data["timeoutResponse"]