zwave-js-server-python 0.35.0__py3-none-any.whl → 0.36.0__py3-none-any.whl
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.
- zwave_js_server/const/__init__.py +22 -2
- zwave_js_server/const/command_class/multilevel_switch.py +8 -0
- zwave_js_server/event.py +1 -1
- zwave_js_server/exceptions.py +23 -14
- zwave_js_server/model/association.py +1 -1
- zwave_js_server/model/controller/__init__.py +121 -62
- zwave_js_server/model/controller/data_model.py +2 -2
- zwave_js_server/model/controller/statistics.py +40 -1
- zwave_js_server/model/device_config.py +2 -2
- zwave_js_server/model/driver.py +4 -0
- zwave_js_server/model/endpoint.py +18 -0
- zwave_js_server/model/log_message.py +2 -2
- zwave_js_server/model/node/__init__.py +19 -7
- zwave_js_server/model/node/data_model.py +2 -2
- zwave_js_server/model/node/statistics.py +56 -4
- zwave_js_server/model/notification.py +47 -0
- zwave_js_server/model/statistics.py +77 -0
- {zwave_js_server_python-0.35.0.dist-info → zwave_js_server_python-0.36.0.dist-info}/METADATA +1 -1
- {zwave_js_server_python-0.35.0.dist-info → zwave_js_server_python-0.36.0.dist-info}/RECORD +23 -25
- {zwave_js_server_python-0.35.0.dist-info → zwave_js_server_python-0.36.0.dist-info}/top_level.txt +0 -1
- scripts/__init__.py +0 -1
- scripts/generate_multilevel_sensor_constants.py +0 -245
- scripts/run_mock_server.py +0 -385
- {zwave_js_server_python-0.35.0.dist-info → zwave_js_server_python-0.36.0.dist-info}/LICENSE +0 -0
- {zwave_js_server_python-0.35.0.dist-info → zwave_js_server_python-0.36.0.dist-info}/WHEEL +0 -0
- {zwave_js_server_python-0.35.0.dist-info → zwave_js_server_python-0.36.0.dist-info}/entry_points.txt +0 -0
|
@@ -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 =
|
|
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 =
|
|
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
|
zwave_js_server/event.py
CHANGED
|
@@ -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:
|
zwave_js_server/exceptions.py
CHANGED
|
@@ -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
|
|
4
|
+
from .const import RssiError
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
-
from .
|
|
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
|
|
163
|
-
"""Exception raised when
|
|
162
|
+
class RssiErrorReceived(BaseZwaveJSServerError):
|
|
163
|
+
"""Exception raised when an RSSI error is received."""
|
|
164
164
|
|
|
165
|
-
def __init__(self,
|
|
166
|
-
"""Initialize an
|
|
167
|
-
self.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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__()
|
|
@@ -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
|
|
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
|
|
68
|
-
"""Return
|
|
69
|
-
return self.data.get("
|
|
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
|
|
118
|
-
"""Return
|
|
119
|
-
return self.data.get("
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
458
|
+
**source_data,
|
|
450
459
|
}
|
|
451
460
|
)
|
|
452
461
|
associations = {}
|
|
453
|
-
for key,
|
|
454
|
-
associations[key] =
|
|
455
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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,
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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(
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
|
699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
141
|
+
compat: Dict[str, Any]
|
|
142
142
|
metadata: DeviceMetadataDataType
|
|
143
143
|
isEmbedded: bool
|
|
144
144
|
|
zwave_js_server/model/driver.py
CHANGED
|
@@ -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] = {}
|
|
@@ -152,7 +157,9 @@ class Node(EventBase):
|
|
|
152
157
|
@property
|
|
153
158
|
def is_secure(self) -> Optional[bool]:
|
|
154
159
|
"""Return the is_secure."""
|
|
155
|
-
|
|
160
|
+
if (is_secure := self.data.get("isSecure")) == "unknown":
|
|
161
|
+
return None
|
|
162
|
+
return is_secure
|
|
156
163
|
|
|
157
164
|
@property
|
|
158
165
|
def protocol_version(self) -> Optional[int]:
|
|
@@ -300,7 +307,7 @@ class Node(EventBase):
|
|
|
300
307
|
"""Update the internal state data."""
|
|
301
308
|
self.data = data
|
|
302
309
|
self._device_config = DeviceConfig(self.data.get("deviceConfig", {}))
|
|
303
|
-
self._statistics = NodeStatistics(self.data.get("statistics"))
|
|
310
|
+
self._statistics = NodeStatistics(self.client, self.data.get("statistics"))
|
|
304
311
|
|
|
305
312
|
# Remove stale values
|
|
306
313
|
value_ids = (_get_value_id_from_dict(self, val) for val in data["values"])
|
|
@@ -778,6 +785,10 @@ class Node(EventBase):
|
|
|
778
785
|
event.data["notification"] = NotificationNotification(
|
|
779
786
|
self, cast(NotificationNotificationDataType, event.data)
|
|
780
787
|
)
|
|
788
|
+
elif command_class == CommandClass.SWITCH_MULTILEVEL:
|
|
789
|
+
event.data["notification"] = MultilevelSwitchNotification(
|
|
790
|
+
self, cast(MultilevelSwitchNotificationDataType, event.data)
|
|
791
|
+
)
|
|
781
792
|
elif command_class == CommandClass.ENTRY_CONTROL:
|
|
782
793
|
event.data["notification"] = EntryControlNotification(
|
|
783
794
|
self, cast(EntryControlNotificationDataType, event.data)
|
|
@@ -787,7 +798,7 @@ class Node(EventBase):
|
|
|
787
798
|
self, cast(PowerLevelNotificationDataType, event.data)
|
|
788
799
|
)
|
|
789
800
|
else:
|
|
790
|
-
|
|
801
|
+
_LOGGER.info("Unhandled notification command class: %s", command_class.name)
|
|
791
802
|
|
|
792
803
|
def handle_firmware_update_progress(self, event: Event) -> None:
|
|
793
804
|
"""Process a node firmware update progress event."""
|
|
@@ -806,5 +817,6 @@ class Node(EventBase):
|
|
|
806
817
|
|
|
807
818
|
def handle_statistics_updated(self, event: Event) -> None:
|
|
808
819
|
"""Process a statistics updated event."""
|
|
809
|
-
|
|
810
|
-
|
|
820
|
+
event.data["statistics_updated"] = self._statistics = NodeStatistics(
|
|
821
|
+
self.client, event.data["statistics"]
|
|
822
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""Data model for a Z-Wave JS node."""
|
|
2
|
-
from typing import List, Optional, Union
|
|
2
|
+
from typing import List, Literal, Optional, Union
|
|
3
3
|
|
|
4
4
|
from ...const import TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED
|
|
5
5
|
from ..device_class import DeviceClassDataType
|
|
@@ -33,7 +33,7 @@ class NodeDataType(TypedDict, total=False):
|
|
|
33
33
|
isRouting: bool
|
|
34
34
|
maxDataRate: int
|
|
35
35
|
supportedDataRates: List[int]
|
|
36
|
-
isSecure: bool
|
|
36
|
+
isSecure: Union[bool, Literal["unknown"]]
|
|
37
37
|
supportsBeaming: bool
|
|
38
38
|
supportsSecurity: bool
|
|
39
39
|
protocolVersion: int
|