From f1af02e2583eca3ef260007776b4ebc778a37df5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 23 Mar 2023 10:38:53 -0400 Subject: [PATCH] Add support for schema 27 (#610) --- test/conftest.py | 2 +- test/fixtures/lock_schlage_be469_state.json | 4 +- test/model/test_controller.py | 90 ++++++++++++++++++- test/model/test_driver.py | 13 +++ test/model/test_node.py | 30 +++++++ test/model/test_value.py | 14 +++ test/test_client.py | 2 +- test/test_dump.py | 2 +- test/test_main.py | 2 +- zwave_js_server/const/__init__.py | 4 +- .../model/controller/statistics.py | 76 +++++++++++++--- zwave_js_server/model/driver.py | 5 ++ zwave_js_server/model/node/__init__.py | 14 +++ zwave_js_server/model/value.py | 12 +++ 14 files changed, 252 insertions(+), 18 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index f4823b686..0223bd144 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -210,7 +210,7 @@ def version_data_fixture(): "serverVersion": "test_server_version", "homeId": "test_home_id", "minSchemaVersion": 0, - "maxSchemaVersion": 26, + "maxSchemaVersion": 27, } diff --git a/test/fixtures/lock_schlage_be469_state.json b/test/fixtures/lock_schlage_be469_state.json index b7c2558ed..dd706210a 100644 --- a/test/fixtures/lock_schlage_be469_state.json +++ b/test/fixtures/lock_schlage_be469_state.json @@ -1643,7 +1643,9 @@ "255": "Enable Beeper" }, "label": "Beeper", - "isFromConfig": true + "isFromConfig": true, + "secret": false, + "stateful": true }, "value": 255 }, diff --git a/test/model/test_controller.py b/test/model/test_controller.py index aa6690ce0..f803d3565 100644 --- a/test/model/test_controller.py +++ b/test/model/test_controller.py @@ -1377,10 +1377,98 @@ async def test_statistics_updated(controller): assert "statistics_updated" in event.data event_stats = event.data["statistics_updated"] assert isinstance(event_stats, ControllerStatistics) - assert controller.statistics.nak == 1 + assert controller.statistics.nak == event_stats.nak == 1 + assert event_stats.background_rssi is None assert controller.statistics == event_stats assert controller.data["statistics"] == statistics_data + statistics_data = { + "messagesTX": 1, + "messagesRX": 1, + "messagesDroppedRX": 1, + "NAK": 1, + "CAN": 1, + "timeoutACK": 1, + "timeoutResponse": 1, + "timeoutCallback": 1, + "messagesDroppedTX": 1, + "backgroundRSSI": { + "timestamp": 1234567890, + "channel0": { + "average": -91, + "current": -92, + }, + "channel1": { + "average": -93, + "current": -94, + }, + }, + } + event = Event( + "statistics updated", + { + "source": "controller", + "event": "statistics updated", + "statistics": statistics_data, + }, + ) + controller.receive_event(event) + event_stats = event.data["statistics_updated"] + assert isinstance(event_stats, ControllerStatistics) + assert event_stats.background_rssi + assert event_stats.background_rssi.timestamp == 1234567890 + assert event_stats.background_rssi.channel_0.average == -91 + assert event_stats.background_rssi.channel_0.current == -92 + assert event_stats.background_rssi.channel_1.average == -93 + assert event_stats.background_rssi.channel_1.current == -94 + assert event_stats.background_rssi.channel_2 is None + + statistics_data = { + "messagesTX": 1, + "messagesRX": 1, + "messagesDroppedRX": 1, + "NAK": 1, + "CAN": 1, + "timeoutACK": 1, + "timeoutResponse": 1, + "timeoutCallback": 1, + "messagesDroppedTX": 1, + "backgroundRSSI": { + "timestamp": 1234567890, + "channel0": { + "average": -81, + "current": -82, + }, + "channel1": { + "average": -83, + "current": -84, + }, + "channel2": { + "average": -85, + "current": -86, + }, + }, + } + event = Event( + "statistics updated", + { + "source": "controller", + "event": "statistics updated", + "statistics": statistics_data, + }, + ) + controller.receive_event(event) + event_stats = event.data["statistics_updated"] + assert isinstance(event_stats, ControllerStatistics) + assert event_stats.background_rssi + assert event_stats.background_rssi.timestamp == 1234567890 + assert event_stats.background_rssi.channel_0.average == -81 + assert event_stats.background_rssi.channel_0.current == -82 + assert event_stats.background_rssi.channel_1.average == -83 + assert event_stats.background_rssi.channel_1.current == -84 + assert event_stats.background_rssi.channel_2.average == -85 + assert event_stats.background_rssi.channel_2.current == -86 + async def test_grant_security_classes(controller, uuid4, mock_command) -> None: """Test controller.grant_security_classes command and event.""" diff --git a/test/model/test_driver.py b/test/model/test_driver.py index c62470556..02134aa1d 100644 --- a/test/model/test_driver.py +++ b/test/model/test_driver.py @@ -377,6 +377,19 @@ async def test_soft_reset(driver, uuid4, mock_command): } +async def test_shutdown(driver, uuid4, mock_command): + """Test driver shutdown command.""" + ack_commands = mock_command({"command": "driver.shutdown"}, {"success": True}) + + assert await driver.async_shutdown() + + assert len(ack_commands) == 1 + assert ack_commands[0] == { + "command": "driver.shutdown", + "messageId": uuid4, + } + + async def test_unknown_event(driver): """Test that an unknown event type causes an exception.""" with pytest.raises(KeyError): diff --git a/test/model/test_node.py b/test/model/test_node.py index eac5025a6..9149143ba 100644 --- a/test/model/test_node.py +++ b/test/model/test_node.py @@ -2015,6 +2015,36 @@ async def test_interview(multisensor_6: node_pkg.Node, uuid4, mock_command): } +async def test_get_value_timestamp(multisensor_6: node_pkg.Node, uuid4, mock_command): + """Test node.get_value_timestamp command.""" + node = multisensor_6 + ack_commands = mock_command( + {"command": "node.get_value_timestamp", "nodeId": node.node_id}, + {"timestamp": 1234567890}, + ) + + val = node.values["52-32-0-targetValue"] + assert await node.async_get_value_timestamp(val) == 1234567890 + + assert len(ack_commands) == 1 + assert ack_commands[0] == { + "command": "node.get_value_timestamp", + "nodeId": node.node_id, + "valueId": {"commandClass": 32, "endpoint": 0, "property": "targetValue"}, + "messageId": uuid4, + } + + assert await node.async_get_value_timestamp("52-112-0-2") == 1234567890 + + assert len(ack_commands) == 2 + assert ack_commands[1] == { + "command": "node.get_value_timestamp", + "nodeId": node.node_id, + "valueId": {"commandClass": 112, "endpoint": 0, "property": 2}, + "messageId": uuid4, + } + + async def test_unknown_event(multisensor_6: node_pkg.Node): """Test that an unknown event type causes an exception.""" with pytest.raises(KeyError): diff --git a/test/model/test_value.py b/test/model/test_value.py index 22cbd90b8..55aecb267 100644 --- a/test/model/test_value.py +++ b/test/model/test_value.py @@ -53,3 +53,17 @@ def test_allow_manual_entry(client, inovelli_switch_state): zwave_value = config_values[value_id] assert zwave_value.configuration_value_type == ConfigurationValueType.ENUMERATED + + +def test_stateful(lock_schlage_be469): + """Test the stateful property for a value.""" + node = lock_schlage_be469 + zwave_value = node.values["20-112-0-3"] + assert not zwave_value.metadata.secret + + +def test_secret(lock_schlage_be469): + """Test the secret property for a value.""" + node = lock_schlage_be469 + zwave_value = node.values["20-112-0-3"] + assert zwave_value.metadata.stateful diff --git a/test/test_client.py b/test/test_client.py index 1713522b1..394620a7d 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -452,7 +452,7 @@ async def test_additional_user_agent_components(client_session, url): { "command": "initialize", "messageId": "initialize", - "schemaVersion": 26, + "schemaVersion": 27, "additionalUserAgentComponents": { "zwave-js-server-python": __version__, "foo": "bar", diff --git a/test/test_dump.py b/test/test_dump.py index c8b535e2a..61407cfd2 100644 --- a/test/test_dump.py +++ b/test/test_dump.py @@ -105,7 +105,7 @@ async def test_dump_additional_user_agent_components( { "command": "initialize", "messageId": "initialize", - "schemaVersion": 26, + "schemaVersion": 27, "additionalUserAgentComponents": { "zwave-js-server-python": __version__, "foo": "bar", diff --git a/test/test_main.py b/test/test_main.py index 7985b48a3..dcac185d5 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -55,7 +55,7 @@ def test_dump_state( assert captured.out == ( "{'type': 'version', 'driverVersion': 'test_driver_version', " "'serverVersion': 'test_server_version', 'homeId': 'test_home_id', " - "'minSchemaVersion': 0, 'maxSchemaVersion': 26}\n" + "'minSchemaVersion': 0, 'maxSchemaVersion': 27}\n" "{'type': 'result', 'success': True, 'result': {}, 'messageId': 'initialize'}\n" "test_result\n" ) diff --git a/zwave_js_server/const/__init__.py b/zwave_js_server/const/__init__.py index 141c98db6..4b979991c 100644 --- a/zwave_js_server/const/__init__.py +++ b/zwave_js_server/const/__init__.py @@ -8,9 +8,9 @@ __version__ = metadata.version(PACKAGE_NAME) # minimal server schema version we can handle -MIN_SERVER_SCHEMA_VERSION = 26 +MIN_SERVER_SCHEMA_VERSION = 27 # max server schema version we can handle (and our code is compatible with) -MAX_SERVER_SCHEMA_VERSION = 26 +MAX_SERVER_SCHEMA_VERSION = 27 VALUE_UNKNOWN = "unknown" diff --git a/zwave_js_server/model/controller/statistics.py b/zwave_js_server/model/controller/statistics.py index 57249991f..4c8541f10 100644 --- a/zwave_js_server/model/controller/statistics.py +++ b/zwave_js_server/model/controller/statistics.py @@ -34,19 +34,72 @@ def __post_init__(self) -> None: self.nlwr = RouteStatistics(self.client, nlwr) -class ControllerStatisticsDataType(TypedDict): +class ChannelRSSIDataType(TypedDict): + """Represent a channel RSSI data dict type.""" + + average: int + current: int + + +class BackgroundRSSIDataType(TypedDict, total=False): + """Represent a background RSSI data dict type.""" + + # https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/controller/ControllerStatistics.ts#L40 + timestamp: int # required + channel0: ChannelRSSIDataType # required + channel1: ChannelRSSIDataType # required + channel2: ChannelRSSIDataType + + +class ControllerStatisticsDataType(TypedDict, total=False): """Represent a controller statistics data dict type.""" # https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/controller/ControllerStatistics.ts#L20-L39 - messagesTX: int - messagesRX: int - messagesDroppedTX: int - messagesDroppedRX: int - NAK: int - CAN: int - timeoutACK: int - timeoutResponse: int - timeoutCallback: int + messagesTX: int # required + messagesRX: int # required + messagesDroppedTX: int # required + messagesDroppedRX: int # required + NAK: int # required + CAN: int # required + timeoutACK: int # required + timeoutResponse: int # required + timeoutCallback: int # required + backgroundRSSI: BackgroundRSSIDataType + + +@dataclass +class ChannelRSSI: + """Represent a channel RSSI.""" + + data: ChannelRSSIDataType + average: int = field(init=False) + current: int = field(init=False) + + def __post_init__(self) -> None: + """Post initialize.""" + self.average = self.data["average"] + self.current = self.data["current"] + + +@dataclass +class BackgroundRSSI: + """Represent a background RSSI update.""" + + data: BackgroundRSSIDataType + timestamp: int = field(init=False) + channel_0: ChannelRSSI = field(init=False) + channel_1: ChannelRSSI = field(init=False) + channel_2: ChannelRSSI | None = field(init=False) + + def __post_init__(self) -> None: + """Post initialize.""" + self.timestamp = self.data["timestamp"] + self.channel_0 = ChannelRSSI(self.data["channel0"]) + self.channel_1 = ChannelRSSI(self.data["channel1"]) + if not (channel_2 := self.data.get("channel2")): + self.channel_2 = None + return + self.channel_2 = ChannelRSSI(channel_2) @dataclass @@ -63,6 +116,7 @@ class ControllerStatistics: timeout_ack: int = field(init=False) timeout_response: int = field(init=False) timeout_callback: int = field(init=False) + background_rssi: BackgroundRSSI | None = field(init=False, default=None) def __post_init__(self) -> None: """Post initialize.""" @@ -86,3 +140,5 @@ def __post_init__(self) -> None: self.timeout_ack = data["timeoutACK"] self.timeout_response = data["timeoutResponse"] self.timeout_callback = data["timeoutCallback"] + if background_rssi := data.get("backgroundRSSI"): + self.background_rssi = BackgroundRSSI(background_rssi) diff --git a/zwave_js_server/model/driver.py b/zwave_js_server/model/driver.py index 1884ae1b5..b966a21e0 100644 --- a/zwave_js_server/model/driver.py +++ b/zwave_js_server/model/driver.py @@ -180,6 +180,11 @@ async def async_soft_reset(self) -> None: """Send command to soft reset controller.""" await self._async_send_command("soft_reset", require_schema=25) + async def async_shutdown(self) -> bool: + """Send command to shutdown controller.""" + data = await self._async_send_command("shutdown", require_schema=27) + return cast(bool, data["success"]) + def handle_logging(self, event: Event) -> None: """Process a driver logging event.""" event.data["log_message"] = LogMessage(cast(LogMessageDataType, event.data)) diff --git a/zwave_js_server/model/node/__init__.py b/zwave_js_server/model/node/__init__.py index 6dded860b..f92790857 100644 --- a/zwave_js_server/model/node/__init__.py +++ b/zwave_js_server/model/node/__init__.py @@ -757,6 +757,20 @@ async def async_interview(self) -> None: require_schema=22, ) + async def async_get_value_timestamp(self, val: Value | str) -> int: + """Send getValueTimestamp command to Node for given value (or value_id).""" + # a value may be specified as value_id or the value itself + if not isinstance(val, Value): + val = self.values[val] + data = await self.async_send_command( + "get_value_timestamp", + valueId=_get_value_id_dict_from_value_data(val.data), + require_schema=27, + wait_for_result=True, + ) + assert data + return cast(int, data["timestamp"]) + def handle_test_powerlevel_progress(self, event: Event) -> None: """Process a test power level progress event.""" event.data["test_power_level_progress"] = TestPowerLevelProgress( diff --git a/zwave_js_server/model/value.py b/zwave_js_server/model/value.py index 3bf80c8fb..683ae90da 100644 --- a/zwave_js_server/model/value.py +++ b/zwave_js_server/model/value.py @@ -27,6 +27,8 @@ class MetaDataType(TypedDict, total=False): valueChangeOptions: list[str] allowManualEntry: bool valueSize: int + stateful: bool + secret: bool class ValueDataType(TypedDict, total=False): @@ -153,6 +155,16 @@ def value_size(self) -> int | None: """Return valueSize.""" return self.data.get("valueSize") + @property + def stateful(self) -> bool | None: + """Return stateful.""" + return self.data.get("stateful") + + @property + def secret(self) -> bool | None: + """Return secret.""" + return self.data.get("secret") + def update(self, data: MetaDataType) -> None: """Update data.""" self.data.update(data)