Skip to content

Commit d101500

Browse files
Cache device class data (#958)
Co-authored-by: Martin Hjelmare <[email protected]>
1 parent 1c266c6 commit d101500

9 files changed

+122
-56
lines changed

test/fixtures/climate_radio_thermostat_ct100_plus_state.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"generic": { "key": 2, "label": "Thermostat" },
1111
"specific": { "key": 3, "label": "Thermostat General V2" },
1212
"mandatorySupportedCCs": [],
13-
"mandatoryControlCCs": []
13+
"mandatoryControlledCCs": []
1414
},
1515
"isListening": true,
1616
"isFrequentListening": false,

test/fixtures/cover_qubino_shutter_state.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"generic": { "key": 2, "label": "Multilevel Switch" },
1111
"specific": { "key": 3, "label": "Motor Control Class C" },
1212
"mandatorySupportedCCs": [],
13-
"mandatoryControlCCs": []
13+
"mandatoryControlledCCs": []
1414
},
1515
"isListening": true,
1616
"isFrequentListening": false,

test/fixtures/idl_101_lock_state.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
"status": 4,
77
"ready": true,
88
"deviceClass": {
9-
"basic": "Routing Slave",
10-
"generic": "Entry Control",
11-
"specific": "Secure Keypad Door Lock",
9+
"basic": { "key": 4, "label": "Routing Slave" },
10+
"generic": { "key": 2, "label": "Entry Control" },
11+
"specific": { "key": 3, "label": "Secure Keypad Door Lock" },
1212
"mandatorySupportedCCs": [
1313
"Basic",
1414
"Door Lock",
@@ -17,7 +17,7 @@
1717
"Security",
1818
"Version"
1919
],
20-
"mandatoryControlCCs": []
20+
"mandatoryControlledCCs": []
2121
},
2222
"isListening": false,
2323
"isFrequentListening": true,

test/fixtures/lock_schlage_be469_state.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"generic": { "key": 2, "label": "Entry Control" },
99
"specific": { "key": 3, "label": "Secure Keypad Door Lock" },
1010
"mandatorySupportedCCs": [],
11-
"mandatoryControlCCs": []
11+
"mandatoryControlledCCs": []
1212
},
1313
"isListening": false,
1414
"isFrequentListening": true,

test/fixtures/unparseable_json_string_value_state.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"generic": { "key": 2, "label": "Entry Control" },
99
"specific": { "key": 3, "label": "Secure Keypad Door Lock" },
1010
"mandatorySupportedCCs": [],
11-
"mandatoryControlCCs": []
11+
"mandatoryControlledCCs": []
1212
},
1313
"isListening": false,
1414
"isFrequentListening": true,

test/model/test_node.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,57 @@ def test_node_inclusion(multisensor_6_state):
611611
assert node.device_config.manufacturer == "AEON Labs"
612612
assert len(node.values) > 0
613613

614+
new_state = deepcopy(multisensor_6_state)
615+
new_state["values"].append(
616+
{
617+
"commandClassName": "Binary Sensor",
618+
"commandClass": 48,
619+
"endpoint": 0,
620+
"property": "test",
621+
"propertyName": "test",
622+
"metadata": {
623+
"type": "boolean",
624+
"readable": True,
625+
"writeable": False,
626+
"label": "Any",
627+
"ccSpecific": {"sensorType": 255},
628+
},
629+
"value": False,
630+
}
631+
)
632+
new_state["endpoints"].append(
633+
{"nodeId": 52, "index": 1, "installerIcon": 3079, "userIcon": 3079}
634+
)
635+
636+
event = Event(
637+
"ready",
638+
{
639+
"event": "ready",
640+
"source": "node",
641+
"nodeId": node.node_id,
642+
"nodeState": new_state,
643+
"result": [],
644+
},
645+
)
646+
node.receive_event(event)
647+
assert "52-48-0-test" in node.values
648+
assert 1 in node.endpoints
649+
650+
new_state = deepcopy(new_state)
651+
new_state["endpoints"].pop(1)
652+
event = Event(
653+
"ready",
654+
{
655+
"event": "ready",
656+
"source": "node",
657+
"nodeId": node.node_id,
658+
"nodeState": multisensor_6_state,
659+
"result": [],
660+
},
661+
)
662+
node.receive_event(event)
663+
assert 1 not in node.endpoints
664+
614665

615666
def test_node_ready_event(switch_enbrighten_zw3010_state):
616667
"""Emulate a node ready event."""

zwave_js_server/model/device_class.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,29 +40,33 @@ class DeviceClass:
4040

4141
def __init__(self, data: DeviceClassDataType) -> None:
4242
"""Initialize."""
43-
self.data = data
43+
self._basic = DeviceClassItem(**data["basic"])
44+
self._generic = DeviceClassItem(**data["generic"])
45+
self._specific = DeviceClassItem(**data["specific"])
46+
self._mandatory_supported_ccs: list[int] = data["mandatorySupportedCCs"]
47+
self._mandatory_controlled_ccs: list[int] = data["mandatoryControlledCCs"]
4448

4549
@property
4650
def basic(self) -> DeviceClassItem:
4751
"""Return basic DeviceClass."""
48-
return DeviceClassItem(**self.data["basic"])
52+
return self._basic
4953

5054
@property
5155
def generic(self) -> DeviceClassItem:
5256
"""Return generic DeviceClass."""
53-
return DeviceClassItem(**self.data["generic"])
57+
return self._generic
5458

5559
@property
5660
def specific(self) -> DeviceClassItem:
5761
"""Return specific DeviceClass."""
58-
return DeviceClassItem(**self.data["specific"])
62+
return self._specific
5963

6064
@property
6165
def mandatory_supported_ccs(self) -> list[int]:
6266
"""Return list of mandatory Supported CC id's."""
63-
return self.data["mandatorySupportedCCs"]
67+
return self._mandatory_supported_ccs
6468

6569
@property
6670
def mandatory_controlled_ccs(self) -> list[int]:
6771
"""Return list of mandatory Controlled CC id's."""
68-
return self.data["mandatoryControlledCCs"]
72+
return self._mandatory_controlled_ccs

zwave_js_server/model/endpoint.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(
5757
self.node = node
5858
self.data: EndpointDataType = data
5959
self.values: dict[str, ConfigurationValue | Value] = {}
60+
self._device_class: DeviceClass | None = None
6061
self.update(data, values)
6162

6263
def __repr__(self) -> str:
@@ -90,9 +91,7 @@ def index(self) -> int:
9091
@property
9192
def device_class(self) -> DeviceClass | None:
9293
"""Return the device_class."""
93-
if (device_class := self.data.get("deviceClass")) is None:
94-
return None
95-
return DeviceClass(device_class)
94+
return self._device_class
9695

9796
@property
9897
def installer_icon(self) -> int | None:
@@ -119,6 +118,10 @@ def update(
119118
) -> None:
120119
"""Update the endpoint data."""
121120
self.data = data
121+
if (device_class := self.data.get("deviceClass")) is None:
122+
self._device_class = None
123+
else:
124+
self._device_class = DeviceClass(device_class)
122125

123126
# Remove stale values
124127
self.values = {

zwave_js_server/model/node/__init__.py

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from ..command_class import CommandClassInfo
2828
from ..device_class import DeviceClass
2929
from ..device_config import DeviceConfig
30-
from ..endpoint import Endpoint
30+
from ..endpoint import Endpoint, EndpointDataType
3131
from ..notification import (
3232
EntryControlNotification,
3333
EntryControlNotificationDataType,
@@ -115,6 +115,7 @@ def __init__(self, client: Client, data: NodeDataType) -> None:
115115
client, data.get("statistics", DEFAULT_NODE_STATISTICS)
116116
)
117117
self._firmware_update_progress: NodeFirmwareUpdateProgress | None = None
118+
self._device_class: DeviceClass | None = None
118119
self._last_seen: datetime | None = None
119120
self.values: dict[str, ConfigurationValue | Value] = {}
120121
self.endpoints: dict[int, Endpoint] = {}
@@ -150,9 +151,7 @@ def index(self) -> int:
150151
@property
151152
def device_class(self) -> DeviceClass | None:
152153
"""Return the device_class."""
153-
if (device_class := self.data.get("deviceClass")) is None:
154-
return None
155-
return DeviceClass(device_class)
154+
return self._device_class
156155

157156
@property
158157
def installer_icon(self) -> int | None:
@@ -372,21 +371,35 @@ def default_transition_duration(self) -> int | float | None:
372371
"""Return the default transition duration."""
373372
return self.data.get("defaultTransitionDuration")
374373

375-
def update(self, data: NodeDataType) -> None:
376-
"""Update the internal state data."""
377-
self.data = copy.deepcopy(data)
378-
self._device_config = DeviceConfig(self.data.get("deviceConfig", {}))
379-
self._statistics = NodeStatistics(
380-
self.client, self.data.get("statistics", DEFAULT_NODE_STATISTICS)
381-
)
382-
if last_seen := data.get("lastSeen"):
383-
self._last_seen = datetime.fromisoformat(last_seen)
384-
if not self._statistics.last_seen:
385-
self._statistics.last_seen = self.last_seen
374+
def _update_endpoints(self, endpoints: list[EndpointDataType]) -> None:
375+
"""Update the endpoints data."""
376+
new_endpoints_data = {endpoint["index"]: endpoint for endpoint in endpoints}
377+
new_endpoint_idxs = set(new_endpoints_data)
378+
stale_endpoint_idxs = set(self.endpoints) - new_endpoint_idxs
379+
380+
# Remove stale endpoints
381+
for endpoint_idx in stale_endpoint_idxs:
382+
self.endpoints.pop(endpoint_idx)
383+
384+
# Add new endpoints or update existing ones
385+
for endpoint_idx in new_endpoint_idxs:
386+
endpoint = new_endpoints_data[endpoint_idx]
387+
values = {
388+
value_id: value
389+
for value_id, value in self.values.items()
390+
if self.index == value.endpoint
391+
}
392+
if endpoint_idx in self.endpoints:
393+
self.endpoints[endpoint_idx].update(endpoint, values)
394+
else:
395+
self.endpoints[endpoint_idx] = Endpoint(
396+
self.client, self, endpoint, values
397+
)
386398

399+
def _update_values(self, values: list[ValueDataType]) -> None:
400+
"""Update the values data."""
387401
new_values_data = {
388-
_get_value_id_str_from_dict(self, val): val
389-
for val in self.data.pop("values")
402+
_get_value_id_str_from_dict(self, val): val for val in values
390403
}
391404
new_value_ids = set(new_values_data)
392405
stale_value_ids = set(self.values) - new_value_ids
@@ -413,30 +426,25 @@ def update(self, data: NodeDataType) -> None:
413426
# If we can't parse the value, don't store it
414427
pass
415428

416-
new_endpoints_data = {
417-
endpoint["index"]: endpoint for endpoint in self.data.pop("endpoints")
418-
}
419-
new_endpoint_idxs = set(new_endpoints_data)
420-
stale_endpoint_idxs = set(self.endpoints) - new_endpoint_idxs
429+
def update(self, data: NodeDataType) -> None:
430+
"""Update the internal state data."""
431+
self.data = copy.deepcopy(data)
432+
self._device_config = DeviceConfig(self.data.get("deviceConfig", {}))
433+
if (device_class := self.data.get("deviceClass")) is None:
434+
self._device_class = None
435+
else:
436+
self._device_class = DeviceClass(device_class)
421437

422-
# Remove stale endpoints
423-
for endpoint_idx in stale_endpoint_idxs:
424-
self.endpoints.pop(endpoint_idx)
438+
self._statistics = NodeStatistics(
439+
self.client, self.data.get("statistics", DEFAULT_NODE_STATISTICS)
440+
)
441+
if last_seen := data.get("lastSeen"):
442+
self._last_seen = datetime.fromisoformat(last_seen)
443+
if not self._statistics.last_seen:
444+
self._statistics.last_seen = self.last_seen
425445

426-
# Add new endpoints or update existing ones
427-
for endpoint_idx in new_endpoint_idxs - stale_endpoint_idxs:
428-
endpoint = new_endpoints_data[endpoint_idx]
429-
values = {
430-
value_id: value
431-
for value_id, value in self.values.items()
432-
if self.index == value.endpoint
433-
}
434-
if endpoint_idx in self.endpoints:
435-
self.endpoints[endpoint_idx].update(endpoint, values)
436-
else:
437-
self.endpoints[endpoint_idx] = Endpoint(
438-
self.client, self, endpoint, values
439-
)
446+
self._update_values(self.data.pop("values"))
447+
self._update_endpoints(self.data.pop("endpoints"))
440448

441449
def get_command_class_values(
442450
self, command_class: CommandClass, endpoint: int | None = None

0 commit comments

Comments
 (0)