Skip to content

Commit

Permalink
Add support for schema 27 (#610)
Browse files Browse the repository at this point in the history
  • Loading branch information
raman325 authored Mar 23, 2023
1 parent 8aeeaf8 commit f1af02e
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 18 deletions.
2 changes: 1 addition & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def version_data_fixture():
"serverVersion": "test_server_version",
"homeId": "test_home_id",
"minSchemaVersion": 0,
"maxSchemaVersion": 26,
"maxSchemaVersion": 27,
}


Expand Down
4 changes: 3 additions & 1 deletion test/fixtures/lock_schlage_be469_state.json
Original file line number Diff line number Diff line change
Expand Up @@ -1643,7 +1643,9 @@
"255": "Enable Beeper"
},
"label": "Beeper",
"isFromConfig": true
"isFromConfig": true,
"secret": false,
"stateful": true
},
"value": 255
},
Expand Down
90 changes: 89 additions & 1 deletion test/model/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
13 changes: 13 additions & 0 deletions test/model/test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
30 changes: 30 additions & 0 deletions test/model/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
14 changes: 14 additions & 0 deletions test/model/test_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion test/test_dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion test/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
4 changes: 2 additions & 2 deletions zwave_js_server/const/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
76 changes: 66 additions & 10 deletions zwave_js_server/model/controller/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""
Expand All @@ -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)
5 changes: 5 additions & 0 deletions zwave_js_server/model/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
14 changes: 14 additions & 0 deletions zwave_js_server/model/node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 12 additions & 0 deletions zwave_js_server/model/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit f1af02e

Please sign in to comment.