Skip to content

Commit f1af02e

Browse files
authored
Add support for schema 27 (#610)
1 parent 8aeeaf8 commit f1af02e

14 files changed

+252
-18
lines changed

test/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def version_data_fixture():
210210
"serverVersion": "test_server_version",
211211
"homeId": "test_home_id",
212212
"minSchemaVersion": 0,
213-
"maxSchemaVersion": 26,
213+
"maxSchemaVersion": 27,
214214
}
215215

216216

test/fixtures/lock_schlage_be469_state.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1643,7 +1643,9 @@
16431643
"255": "Enable Beeper"
16441644
},
16451645
"label": "Beeper",
1646-
"isFromConfig": true
1646+
"isFromConfig": true,
1647+
"secret": false,
1648+
"stateful": true
16471649
},
16481650
"value": 255
16491651
},

test/model/test_controller.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1377,10 +1377,98 @@ async def test_statistics_updated(controller):
13771377
assert "statistics_updated" in event.data
13781378
event_stats = event.data["statistics_updated"]
13791379
assert isinstance(event_stats, ControllerStatistics)
1380-
assert controller.statistics.nak == 1
1380+
assert controller.statistics.nak == event_stats.nak == 1
1381+
assert event_stats.background_rssi is None
13811382
assert controller.statistics == event_stats
13821383
assert controller.data["statistics"] == statistics_data
13831384

1385+
statistics_data = {
1386+
"messagesTX": 1,
1387+
"messagesRX": 1,
1388+
"messagesDroppedRX": 1,
1389+
"NAK": 1,
1390+
"CAN": 1,
1391+
"timeoutACK": 1,
1392+
"timeoutResponse": 1,
1393+
"timeoutCallback": 1,
1394+
"messagesDroppedTX": 1,
1395+
"backgroundRSSI": {
1396+
"timestamp": 1234567890,
1397+
"channel0": {
1398+
"average": -91,
1399+
"current": -92,
1400+
},
1401+
"channel1": {
1402+
"average": -93,
1403+
"current": -94,
1404+
},
1405+
},
1406+
}
1407+
event = Event(
1408+
"statistics updated",
1409+
{
1410+
"source": "controller",
1411+
"event": "statistics updated",
1412+
"statistics": statistics_data,
1413+
},
1414+
)
1415+
controller.receive_event(event)
1416+
event_stats = event.data["statistics_updated"]
1417+
assert isinstance(event_stats, ControllerStatistics)
1418+
assert event_stats.background_rssi
1419+
assert event_stats.background_rssi.timestamp == 1234567890
1420+
assert event_stats.background_rssi.channel_0.average == -91
1421+
assert event_stats.background_rssi.channel_0.current == -92
1422+
assert event_stats.background_rssi.channel_1.average == -93
1423+
assert event_stats.background_rssi.channel_1.current == -94
1424+
assert event_stats.background_rssi.channel_2 is None
1425+
1426+
statistics_data = {
1427+
"messagesTX": 1,
1428+
"messagesRX": 1,
1429+
"messagesDroppedRX": 1,
1430+
"NAK": 1,
1431+
"CAN": 1,
1432+
"timeoutACK": 1,
1433+
"timeoutResponse": 1,
1434+
"timeoutCallback": 1,
1435+
"messagesDroppedTX": 1,
1436+
"backgroundRSSI": {
1437+
"timestamp": 1234567890,
1438+
"channel0": {
1439+
"average": -81,
1440+
"current": -82,
1441+
},
1442+
"channel1": {
1443+
"average": -83,
1444+
"current": -84,
1445+
},
1446+
"channel2": {
1447+
"average": -85,
1448+
"current": -86,
1449+
},
1450+
},
1451+
}
1452+
event = Event(
1453+
"statistics updated",
1454+
{
1455+
"source": "controller",
1456+
"event": "statistics updated",
1457+
"statistics": statistics_data,
1458+
},
1459+
)
1460+
controller.receive_event(event)
1461+
event_stats = event.data["statistics_updated"]
1462+
assert isinstance(event_stats, ControllerStatistics)
1463+
assert event_stats.background_rssi
1464+
assert event_stats.background_rssi.timestamp == 1234567890
1465+
assert event_stats.background_rssi.channel_0.average == -81
1466+
assert event_stats.background_rssi.channel_0.current == -82
1467+
assert event_stats.background_rssi.channel_1.average == -83
1468+
assert event_stats.background_rssi.channel_1.current == -84
1469+
assert event_stats.background_rssi.channel_2.average == -85
1470+
assert event_stats.background_rssi.channel_2.current == -86
1471+
13841472

13851473
async def test_grant_security_classes(controller, uuid4, mock_command) -> None:
13861474
"""Test controller.grant_security_classes command and event."""

test/model/test_driver.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,19 @@ async def test_soft_reset(driver, uuid4, mock_command):
377377
}
378378

379379

380+
async def test_shutdown(driver, uuid4, mock_command):
381+
"""Test driver shutdown command."""
382+
ack_commands = mock_command({"command": "driver.shutdown"}, {"success": True})
383+
384+
assert await driver.async_shutdown()
385+
386+
assert len(ack_commands) == 1
387+
assert ack_commands[0] == {
388+
"command": "driver.shutdown",
389+
"messageId": uuid4,
390+
}
391+
392+
380393
async def test_unknown_event(driver):
381394
"""Test that an unknown event type causes an exception."""
382395
with pytest.raises(KeyError):

test/model/test_node.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,6 +2015,36 @@ async def test_interview(multisensor_6: node_pkg.Node, uuid4, mock_command):
20152015
}
20162016

20172017

2018+
async def test_get_value_timestamp(multisensor_6: node_pkg.Node, uuid4, mock_command):
2019+
"""Test node.get_value_timestamp command."""
2020+
node = multisensor_6
2021+
ack_commands = mock_command(
2022+
{"command": "node.get_value_timestamp", "nodeId": node.node_id},
2023+
{"timestamp": 1234567890},
2024+
)
2025+
2026+
val = node.values["52-32-0-targetValue"]
2027+
assert await node.async_get_value_timestamp(val) == 1234567890
2028+
2029+
assert len(ack_commands) == 1
2030+
assert ack_commands[0] == {
2031+
"command": "node.get_value_timestamp",
2032+
"nodeId": node.node_id,
2033+
"valueId": {"commandClass": 32, "endpoint": 0, "property": "targetValue"},
2034+
"messageId": uuid4,
2035+
}
2036+
2037+
assert await node.async_get_value_timestamp("52-112-0-2") == 1234567890
2038+
2039+
assert len(ack_commands) == 2
2040+
assert ack_commands[1] == {
2041+
"command": "node.get_value_timestamp",
2042+
"nodeId": node.node_id,
2043+
"valueId": {"commandClass": 112, "endpoint": 0, "property": 2},
2044+
"messageId": uuid4,
2045+
}
2046+
2047+
20182048
async def test_unknown_event(multisensor_6: node_pkg.Node):
20192049
"""Test that an unknown event type causes an exception."""
20202050
with pytest.raises(KeyError):

test/model/test_value.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,17 @@ def test_allow_manual_entry(client, inovelli_switch_state):
5353
zwave_value = config_values[value_id]
5454

5555
assert zwave_value.configuration_value_type == ConfigurationValueType.ENUMERATED
56+
57+
58+
def test_stateful(lock_schlage_be469):
59+
"""Test the stateful property for a value."""
60+
node = lock_schlage_be469
61+
zwave_value = node.values["20-112-0-3"]
62+
assert not zwave_value.metadata.secret
63+
64+
65+
def test_secret(lock_schlage_be469):
66+
"""Test the secret property for a value."""
67+
node = lock_schlage_be469
68+
zwave_value = node.values["20-112-0-3"]
69+
assert zwave_value.metadata.stateful

test/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ async def test_additional_user_agent_components(client_session, url):
452452
{
453453
"command": "initialize",
454454
"messageId": "initialize",
455-
"schemaVersion": 26,
455+
"schemaVersion": 27,
456456
"additionalUserAgentComponents": {
457457
"zwave-js-server-python": __version__,
458458
"foo": "bar",

test/test_dump.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ async def test_dump_additional_user_agent_components(
105105
{
106106
"command": "initialize",
107107
"messageId": "initialize",
108-
"schemaVersion": 26,
108+
"schemaVersion": 27,
109109
"additionalUserAgentComponents": {
110110
"zwave-js-server-python": __version__,
111111
"foo": "bar",

test/test_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_dump_state(
5555
assert captured.out == (
5656
"{'type': 'version', 'driverVersion': 'test_driver_version', "
5757
"'serverVersion': 'test_server_version', 'homeId': 'test_home_id', "
58-
"'minSchemaVersion': 0, 'maxSchemaVersion': 26}\n"
58+
"'minSchemaVersion': 0, 'maxSchemaVersion': 27}\n"
5959
"{'type': 'result', 'success': True, 'result': {}, 'messageId': 'initialize'}\n"
6060
"test_result\n"
6161
)

zwave_js_server/const/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
__version__ = metadata.version(PACKAGE_NAME)
99

1010
# minimal server schema version we can handle
11-
MIN_SERVER_SCHEMA_VERSION = 26
11+
MIN_SERVER_SCHEMA_VERSION = 27
1212
# max server schema version we can handle (and our code is compatible with)
13-
MAX_SERVER_SCHEMA_VERSION = 26
13+
MAX_SERVER_SCHEMA_VERSION = 27
1414

1515
VALUE_UNKNOWN = "unknown"
1616

zwave_js_server/model/controller/statistics.py

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,72 @@ def __post_init__(self) -> None:
3434
self.nlwr = RouteStatistics(self.client, nlwr)
3535

3636

37-
class ControllerStatisticsDataType(TypedDict):
37+
class ChannelRSSIDataType(TypedDict):
38+
"""Represent a channel RSSI data dict type."""
39+
40+
average: int
41+
current: int
42+
43+
44+
class BackgroundRSSIDataType(TypedDict, total=False):
45+
"""Represent a background RSSI data dict type."""
46+
47+
# https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/controller/ControllerStatistics.ts#L40
48+
timestamp: int # required
49+
channel0: ChannelRSSIDataType # required
50+
channel1: ChannelRSSIDataType # required
51+
channel2: ChannelRSSIDataType
52+
53+
54+
class ControllerStatisticsDataType(TypedDict, total=False):
3855
"""Represent a controller statistics data dict type."""
3956

4057
# https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/controller/ControllerStatistics.ts#L20-L39
41-
messagesTX: int
42-
messagesRX: int
43-
messagesDroppedTX: int
44-
messagesDroppedRX: int
45-
NAK: int
46-
CAN: int
47-
timeoutACK: int
48-
timeoutResponse: int
49-
timeoutCallback: int
58+
messagesTX: int # required
59+
messagesRX: int # required
60+
messagesDroppedTX: int # required
61+
messagesDroppedRX: int # required
62+
NAK: int # required
63+
CAN: int # required
64+
timeoutACK: int # required
65+
timeoutResponse: int # required
66+
timeoutCallback: int # required
67+
backgroundRSSI: BackgroundRSSIDataType
68+
69+
70+
@dataclass
71+
class ChannelRSSI:
72+
"""Represent a channel RSSI."""
73+
74+
data: ChannelRSSIDataType
75+
average: int = field(init=False)
76+
current: int = field(init=False)
77+
78+
def __post_init__(self) -> None:
79+
"""Post initialize."""
80+
self.average = self.data["average"]
81+
self.current = self.data["current"]
82+
83+
84+
@dataclass
85+
class BackgroundRSSI:
86+
"""Represent a background RSSI update."""
87+
88+
data: BackgroundRSSIDataType
89+
timestamp: int = field(init=False)
90+
channel_0: ChannelRSSI = field(init=False)
91+
channel_1: ChannelRSSI = field(init=False)
92+
channel_2: ChannelRSSI | None = field(init=False)
93+
94+
def __post_init__(self) -> None:
95+
"""Post initialize."""
96+
self.timestamp = self.data["timestamp"]
97+
self.channel_0 = ChannelRSSI(self.data["channel0"])
98+
self.channel_1 = ChannelRSSI(self.data["channel1"])
99+
if not (channel_2 := self.data.get("channel2")):
100+
self.channel_2 = None
101+
return
102+
self.channel_2 = ChannelRSSI(channel_2)
50103

51104

52105
@dataclass
@@ -63,6 +116,7 @@ class ControllerStatistics:
63116
timeout_ack: int = field(init=False)
64117
timeout_response: int = field(init=False)
65118
timeout_callback: int = field(init=False)
119+
background_rssi: BackgroundRSSI | None = field(init=False, default=None)
66120

67121
def __post_init__(self) -> None:
68122
"""Post initialize."""
@@ -86,3 +140,5 @@ def __post_init__(self) -> None:
86140
self.timeout_ack = data["timeoutACK"]
87141
self.timeout_response = data["timeoutResponse"]
88142
self.timeout_callback = data["timeoutCallback"]
143+
if background_rssi := data.get("backgroundRSSI"):
144+
self.background_rssi = BackgroundRSSI(background_rssi)

zwave_js_server/model/driver.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ async def async_soft_reset(self) -> None:
180180
"""Send command to soft reset controller."""
181181
await self._async_send_command("soft_reset", require_schema=25)
182182

183+
async def async_shutdown(self) -> bool:
184+
"""Send command to shutdown controller."""
185+
data = await self._async_send_command("shutdown", require_schema=27)
186+
return cast(bool, data["success"])
187+
183188
def handle_logging(self, event: Event) -> None:
184189
"""Process a driver logging event."""
185190
event.data["log_message"] = LogMessage(cast(LogMessageDataType, event.data))

zwave_js_server/model/node/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,20 @@ async def async_interview(self) -> None:
757757
require_schema=22,
758758
)
759759

760+
async def async_get_value_timestamp(self, val: Value | str) -> int:
761+
"""Send getValueTimestamp command to Node for given value (or value_id)."""
762+
# a value may be specified as value_id or the value itself
763+
if not isinstance(val, Value):
764+
val = self.values[val]
765+
data = await self.async_send_command(
766+
"get_value_timestamp",
767+
valueId=_get_value_id_dict_from_value_data(val.data),
768+
require_schema=27,
769+
wait_for_result=True,
770+
)
771+
assert data
772+
return cast(int, data["timestamp"])
773+
760774
def handle_test_powerlevel_progress(self, event: Event) -> None:
761775
"""Process a test power level progress event."""
762776
event.data["test_power_level_progress"] = TestPowerLevelProgress(

zwave_js_server/model/value.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class MetaDataType(TypedDict, total=False):
2727
valueChangeOptions: list[str]
2828
allowManualEntry: bool
2929
valueSize: int
30+
stateful: bool
31+
secret: bool
3032

3133

3234
class ValueDataType(TypedDict, total=False):
@@ -153,6 +155,16 @@ def value_size(self) -> int | None:
153155
"""Return valueSize."""
154156
return self.data.get("valueSize")
155157

158+
@property
159+
def stateful(self) -> bool | None:
160+
"""Return stateful."""
161+
return self.data.get("stateful")
162+
163+
@property
164+
def secret(self) -> bool | None:
165+
"""Return secret."""
166+
return self.data.get("secret")
167+
156168
def update(self, data: MetaDataType) -> None:
157169
"""Update data."""
158170
self.data.update(data)

0 commit comments

Comments
 (0)