From 348125fc691ad7038fa487e991f9a29076c1b257 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 25 Apr 2024 01:45:08 -0400 Subject: [PATCH 1/8] Cache device class data --- zwave_js_server/model/device_class.py | 16 ++++++++++------ zwave_js_server/model/endpoint.py | 9 ++++++--- zwave_js_server/model/node/__init__.py | 10 +++++++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/zwave_js_server/model/device_class.py b/zwave_js_server/model/device_class.py index af31eafec..ca638a305 100644 --- a/zwave_js_server/model/device_class.py +++ b/zwave_js_server/model/device_class.py @@ -40,29 +40,33 @@ class DeviceClass: def __init__(self, data: DeviceClassDataType) -> None: """Initialize.""" - self.data = data + self._basic = DeviceClassItem(**data["basic"]) + self._generic = DeviceClassItem(**data["generic"]) + self._specific = DeviceClassItem(**data["specific"]) + self._mandatory_supported_ccs: list[int] = data["mandatorySupportedCCs"] + self._mandatory_controlled_ccs: list[int] = data["mandatoryControlledCCs"] @property def basic(self) -> DeviceClassItem: """Return basic DeviceClass.""" - return DeviceClassItem(**self.data["basic"]) + return self._basic @property def generic(self) -> DeviceClassItem: """Return generic DeviceClass.""" - return DeviceClassItem(**self.data["generic"]) + return self._generic @property def specific(self) -> DeviceClassItem: """Return specific DeviceClass.""" - return DeviceClassItem(**self.data["specific"]) + return self._specific @property def mandatory_supported_ccs(self) -> list[int]: """Return list of mandatory Supported CC id's.""" - return self.data["mandatorySupportedCCs"] + return self._mandatory_supported_ccs @property def mandatory_controlled_ccs(self) -> list[int]: """Return list of mandatory Controlled CC id's.""" - return self.data["mandatoryControlledCCs"] + return self._mandatory_controlled_ccs diff --git a/zwave_js_server/model/endpoint.py b/zwave_js_server/model/endpoint.py index fae3a9825..2356d78f7 100644 --- a/zwave_js_server/model/endpoint.py +++ b/zwave_js_server/model/endpoint.py @@ -57,6 +57,7 @@ def __init__( self.node = node self.data: EndpointDataType = data self.values: dict[str, ConfigurationValue | Value] = {} + self._device_class: None | DeviceClass = None self.update(data, values) def __repr__(self) -> str: @@ -90,9 +91,7 @@ def index(self) -> int: @property def device_class(self) -> DeviceClass | None: """Return the device_class.""" - if (device_class := self.data.get("deviceClass")) is None: - return None - return DeviceClass(device_class) + return self._device_class @property def installer_icon(self) -> int | None: @@ -119,6 +118,10 @@ def update( ) -> None: """Update the endpoint data.""" self.data = data + if (device_class := self.data.get("deviceClass")) is None: + self._device_class = None + else: + self._device_class = DeviceClass(device_class) # Remove stale values self.values = { diff --git a/zwave_js_server/model/node/__init__.py b/zwave_js_server/model/node/__init__.py index 599f57776..01fb990b5 100644 --- a/zwave_js_server/model/node/__init__.py +++ b/zwave_js_server/model/node/__init__.py @@ -115,6 +115,7 @@ def __init__(self, client: Client, data: NodeDataType) -> None: client, data.get("statistics", DEFAULT_NODE_STATISTICS) ) self._firmware_update_progress: NodeFirmwareUpdateProgress | None = None + self._device_class: None | DeviceClass = None self.values: dict[str, ConfigurationValue | Value] = {} self.endpoints: dict[int, Endpoint] = {} self.status_event = asyncio.Event() @@ -149,9 +150,7 @@ def index(self) -> int: @property def device_class(self) -> DeviceClass | None: """Return the device_class.""" - if (device_class := self.data.get("deviceClass")) is None: - return None - return DeviceClass(device_class) + return self._device_class @property def installer_icon(self) -> int | None: @@ -377,6 +376,11 @@ def update(self, data: NodeDataType) -> None: """Update the internal state data.""" self.data = copy.deepcopy(data) self._device_config = DeviceConfig(self.data.get("deviceConfig", {})) + if (device_class := self.data.get("deviceClass")) is None: + self._device_class = None + else: + self._device_class = DeviceClass(device_class) + self._statistics = NodeStatistics( self.client, self.data.get("statistics", DEFAULT_NODE_STATISTICS) ) From bd7e671fd9d054e27e148b31f4f6862319b9ddea Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 25 Apr 2024 02:11:41 -0400 Subject: [PATCH 2/8] Fix fixture data --- .../climate_radio_thermostat_ct100_plus_state.json | 2 +- test/fixtures/cover_qubino_shutter_state.json | 2 +- test/fixtures/idl_101_lock_state.json | 8 ++++---- test/fixtures/lock_schlage_be469_state.json | 2 +- test/fixtures/unparseable_json_string_value_state.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/fixtures/climate_radio_thermostat_ct100_plus_state.json b/test/fixtures/climate_radio_thermostat_ct100_plus_state.json index dcabbc6ad..5e3f8e6db 100644 --- a/test/fixtures/climate_radio_thermostat_ct100_plus_state.json +++ b/test/fixtures/climate_radio_thermostat_ct100_plus_state.json @@ -10,7 +10,7 @@ "generic": { "key": 2, "label": "Thermostat" }, "specific": { "key": 3, "label": "Thermostat General V2" }, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, diff --git a/test/fixtures/cover_qubino_shutter_state.json b/test/fixtures/cover_qubino_shutter_state.json index c83593e17..379960811 100644 --- a/test/fixtures/cover_qubino_shutter_state.json +++ b/test/fixtures/cover_qubino_shutter_state.json @@ -10,7 +10,7 @@ "generic": { "key": 2, "label": "Multilevel Switch" }, "specific": { "key": 3, "label": "Motor Control Class C" }, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, diff --git a/test/fixtures/idl_101_lock_state.json b/test/fixtures/idl_101_lock_state.json index c14af3316..01c801fd5 100644 --- a/test/fixtures/idl_101_lock_state.json +++ b/test/fixtures/idl_101_lock_state.json @@ -6,9 +6,9 @@ "status": 4, "ready": true, "deviceClass": { - "basic": "Routing Slave", - "generic": "Entry Control", - "specific": "Secure Keypad Door Lock", + "basic": { "key": 4, "label": "Routing Slave" }, + "generic": { "key": 2, "label": "Entry Control" }, + "specific": { "key": 3, "label": "Secure Keypad Door Lock" }, "mandatorySupportedCCs": [ "Basic", "Door Lock", @@ -17,7 +17,7 @@ "Security", "Version" ], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, diff --git a/test/fixtures/lock_schlage_be469_state.json b/test/fixtures/lock_schlage_be469_state.json index dd706210a..ddbcf298d 100644 --- a/test/fixtures/lock_schlage_be469_state.json +++ b/test/fixtures/lock_schlage_be469_state.json @@ -8,7 +8,7 @@ "generic": { "key": 2, "label": "Entry Control" }, "specific": { "key": 3, "label": "Secure Keypad Door Lock" }, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, diff --git a/test/fixtures/unparseable_json_string_value_state.json b/test/fixtures/unparseable_json_string_value_state.json index f90426656..7d8a42d11 100644 --- a/test/fixtures/unparseable_json_string_value_state.json +++ b/test/fixtures/unparseable_json_string_value_state.json @@ -8,7 +8,7 @@ "generic": { "key": 2, "label": "Entry Control" }, "specific": { "key": 3, "label": "Secure Keypad Door Lock" }, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, From 9e044d0ba1413c99ace1236b9f1c2a2ba3626212 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:37:39 -0400 Subject: [PATCH 3/8] Refactor to reduce branching --- zwave_js_server/model/node/__init__.py | 82 ++++++++++++++------------ 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/zwave_js_server/model/node/__init__.py b/zwave_js_server/model/node/__init__.py index e15137255..c0c81d0bd 100644 --- a/zwave_js_server/model/node/__init__.py +++ b/zwave_js_server/model/node/__init__.py @@ -371,26 +371,35 @@ def default_transition_duration(self) -> int | float | None: """Return the default transition duration.""" return self.data.get("defaultTransitionDuration") - def update(self, data: NodeDataType) -> None: - """Update the internal state data.""" - self.data = copy.deepcopy(data) - self._device_config = DeviceConfig(self.data.get("deviceConfig", {})) - if (device_class := self.data.get("deviceClass")) is None: - self._device_class = None - else: - self._device_class = DeviceClass(device_class) + def _update_endpoints(self, endpoints: list[Endpoint]) -> None: + """Update the endpoints data.""" + new_endpoints_data = {endpoint["index"]: endpoint for endpoint in endpoints} + new_endpoint_idxs = set(new_endpoints_data) + stale_endpoint_idxs = set(self.endpoints) - new_endpoint_idxs - self._statistics = NodeStatistics( - self.client, self.data.get("statistics", DEFAULT_NODE_STATISTICS) - ) - if last_seen := data.get("lastSeen"): - self._last_seen = datetime.fromisoformat(last_seen) - if not self._statistics.last_seen: - self._statistics.last_seen = self.last_seen + # Remove stale endpoints + for endpoint_idx in stale_endpoint_idxs: + self.endpoints.pop(endpoint_idx) + + # Add new endpoints or update existing ones + for endpoint_idx in new_endpoint_idxs - stale_endpoint_idxs: + endpoint = new_endpoints_data[endpoint_idx] + values = { + value_id: value + for value_id, value in self.values.items() + if self.index == value.endpoint + } + if endpoint_idx in self.endpoints: + self.endpoints[endpoint_idx].update(endpoint, values) + else: + self.endpoints[endpoint_idx] = Endpoint( + self.client, self, endpoint, values + ) + def _update_values(self, values: list[ValueDataType]) -> None: + """Update the internal state data.""" new_values_data = { - _get_value_id_str_from_dict(self, val): val - for val in self.data.pop("values") + _get_value_id_str_from_dict(self, val): val for val in values } new_value_ids = set(new_values_data) stale_value_ids = set(self.values) - new_value_ids @@ -417,30 +426,25 @@ def update(self, data: NodeDataType) -> None: # If we can't parse the value, don't store it pass - new_endpoints_data = { - endpoint["index"]: endpoint for endpoint in self.data.pop("endpoints") - } - new_endpoint_idxs = set(new_endpoints_data) - stale_endpoint_idxs = set(self.endpoints) - new_endpoint_idxs + def update(self, data: NodeDataType) -> None: + """Update the internal state data.""" + self.data = copy.deepcopy(data) + self._device_config = DeviceConfig(self.data.get("deviceConfig", {})) + if (device_class := self.data.get("deviceClass")) is None: + self._device_class = None + else: + self._device_class = DeviceClass(device_class) - # Remove stale endpoints - for endpoint_idx in stale_endpoint_idxs: - self.endpoints.pop(endpoint_idx) + self._statistics = NodeStatistics( + self.client, self.data.get("statistics", DEFAULT_NODE_STATISTICS) + ) + if last_seen := data.get("lastSeen"): + self._last_seen = datetime.fromisoformat(last_seen) + if not self._statistics.last_seen: + self._statistics.last_seen = self.last_seen - # Add new endpoints or update existing ones - for endpoint_idx in new_endpoint_idxs - stale_endpoint_idxs: - endpoint = new_endpoints_data[endpoint_idx] - values = { - value_id: value - for value_id, value in self.values.items() - if self.index == value.endpoint - } - if endpoint_idx in self.endpoints: - self.endpoints[endpoint_idx].update(endpoint, values) - else: - self.endpoints[endpoint_idx] = Endpoint( - self.client, self, endpoint, values - ) + self._update_values(self.data.pop("values")) + self._update_endpoints(self.data.pop("endpoints")) def get_command_class_values( self, command_class: CommandClass, endpoint: int | None = None From 66fed16a24d4cf69916962a9a6806700d4b176fe Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:39:10 -0400 Subject: [PATCH 4/8] fix docstring --- zwave_js_server/model/node/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zwave_js_server/model/node/__init__.py b/zwave_js_server/model/node/__init__.py index c0c81d0bd..b4e204a00 100644 --- a/zwave_js_server/model/node/__init__.py +++ b/zwave_js_server/model/node/__init__.py @@ -397,7 +397,7 @@ def _update_endpoints(self, endpoints: list[Endpoint]) -> None: ) def _update_values(self, values: list[ValueDataType]) -> None: - """Update the internal state data.""" + """Update the values data.""" new_values_data = { _get_value_id_str_from_dict(self, val): val for val in values } From d3b19fac9701900db320ffbf36321a1be8c56a57 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:43:48 -0400 Subject: [PATCH 5/8] Fix typing --- zwave_js_server/model/node/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zwave_js_server/model/node/__init__.py b/zwave_js_server/model/node/__init__.py index b4e204a00..911ecb1d0 100644 --- a/zwave_js_server/model/node/__init__.py +++ b/zwave_js_server/model/node/__init__.py @@ -27,7 +27,7 @@ from ..command_class import CommandClassInfo from ..device_class import DeviceClass from ..device_config import DeviceConfig -from ..endpoint import Endpoint +from ..endpoint import Endpoint, EndpointDataType from ..notification import ( EntryControlNotification, EntryControlNotificationDataType, @@ -371,7 +371,7 @@ def default_transition_duration(self) -> int | float | None: """Return the default transition duration.""" return self.data.get("defaultTransitionDuration") - def _update_endpoints(self, endpoints: list[Endpoint]) -> None: + def _update_endpoints(self, endpoints: list[EndpointDataType]) -> None: """Update the endpoints data.""" new_endpoints_data = {endpoint["index"]: endpoint for endpoint in endpoints} new_endpoint_idxs = set(new_endpoints_data) From cf91eaef6dbc26b1f7a9180db9a2c7b1b2dd0881 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 26 Apr 2024 02:44:39 -0400 Subject: [PATCH 6/8] Update endpoint.py Co-authored-by: Martin Hjelmare --- zwave_js_server/model/endpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zwave_js_server/model/endpoint.py b/zwave_js_server/model/endpoint.py index 2356d78f7..d1dd17f88 100644 --- a/zwave_js_server/model/endpoint.py +++ b/zwave_js_server/model/endpoint.py @@ -57,7 +57,7 @@ def __init__( self.node = node self.data: EndpointDataType = data self.values: dict[str, ConfigurationValue | Value] = {} - self._device_class: None | DeviceClass = None + self._device_class: DeviceClass | None = None self.update(data, values) def __repr__(self) -> str: From c8dca3bffef95021262798f33ed0b8e873896487 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 26 Apr 2024 02:44:43 -0400 Subject: [PATCH 7/8] Update __init__.py Co-authored-by: Martin Hjelmare --- zwave_js_server/model/node/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zwave_js_server/model/node/__init__.py b/zwave_js_server/model/node/__init__.py index 911ecb1d0..e035c95e9 100644 --- a/zwave_js_server/model/node/__init__.py +++ b/zwave_js_server/model/node/__init__.py @@ -115,7 +115,7 @@ def __init__(self, client: Client, data: NodeDataType) -> None: client, data.get("statistics", DEFAULT_NODE_STATISTICS) ) self._firmware_update_progress: NodeFirmwareUpdateProgress | None = None - self._device_class: None | DeviceClass = None + self._device_class: DeviceClass | None = None self._last_seen: datetime | None = None self.values: dict[str, ConfigurationValue | Value] = {} self.endpoints: dict[int, Endpoint] = {} From bf3dccc9ebbeef9482e2ef46de0d7eb76b659b8f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:55:56 -0400 Subject: [PATCH 8/8] Add test coverage and fix noop --- test/model/test_node.py | 51 ++++++++++++++++++++++++++ zwave_js_server/model/node/__init__.py | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/test/model/test_node.py b/test/model/test_node.py index 8069ccd04..277bf5d71 100644 --- a/test/model/test_node.py +++ b/test/model/test_node.py @@ -611,6 +611,57 @@ def test_node_inclusion(multisensor_6_state): assert node.device_config.manufacturer == "AEON Labs" assert len(node.values) > 0 + new_state = deepcopy(multisensor_6_state) + new_state["values"].append( + { + "commandClassName": "Binary Sensor", + "commandClass": 48, + "endpoint": 0, + "property": "test", + "propertyName": "test", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": False, + "label": "Any", + "ccSpecific": {"sensorType": 255}, + }, + "value": False, + } + ) + new_state["endpoints"].append( + {"nodeId": 52, "index": 1, "installerIcon": 3079, "userIcon": 3079} + ) + + event = Event( + "ready", + { + "event": "ready", + "source": "node", + "nodeId": node.node_id, + "nodeState": new_state, + "result": [], + }, + ) + node.receive_event(event) + assert "52-48-0-test" in node.values + assert 1 in node.endpoints + + new_state = deepcopy(new_state) + new_state["endpoints"].pop(1) + event = Event( + "ready", + { + "event": "ready", + "source": "node", + "nodeId": node.node_id, + "nodeState": multisensor_6_state, + "result": [], + }, + ) + node.receive_event(event) + assert 1 not in node.endpoints + def test_node_ready_event(switch_enbrighten_zw3010_state): """Emulate a node ready event.""" diff --git a/zwave_js_server/model/node/__init__.py b/zwave_js_server/model/node/__init__.py index e035c95e9..7af486733 100644 --- a/zwave_js_server/model/node/__init__.py +++ b/zwave_js_server/model/node/__init__.py @@ -382,7 +382,7 @@ def _update_endpoints(self, endpoints: list[EndpointDataType]) -> None: self.endpoints.pop(endpoint_idx) # Add new endpoints or update existing ones - for endpoint_idx in new_endpoint_idxs - stale_endpoint_idxs: + for endpoint_idx in new_endpoint_idxs: endpoint = new_endpoints_data[endpoint_idx] values = { value_id: value