zwave-js-server-python 0.35.0__py3-none-any.whl → 0.35.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
zwave_js_server/event.py CHANGED
@@ -57,7 +57,7 @@ class EventBase:
57
57
 
58
58
  def emit(self, event_name: str, data: dict) -> None:
59
59
  """Run all callbacks for an event."""
60
- for listener in self._listeners.get(event_name, []):
60
+ for listener in self._listeners.get(event_name, []).copy():
61
61
  listener(data)
62
62
 
63
63
  def _handle_event_protocol(self, event: Event) -> None:
@@ -3,7 +3,7 @@ Model for a Zwave Node's device config.
3
3
 
4
4
  https://zwave-js.github.io/node-zwave-js/#/api/node?id=deviceconfig
5
5
  """
6
- from typing import Dict, List, Literal, Optional, Union
6
+ from typing import Any, Dict, List, Literal, Optional, Union
7
7
 
8
8
  from ..const import TYPING_EXTENSION_FOR_TYPEDDICT_REQUIRED
9
9
 
@@ -138,7 +138,7 @@ class DeviceConfigDataType(TypedDict, total=False):
138
138
  paramInformation: Dict[str, dict]
139
139
  supportsZWavePlus: bool
140
140
  proprietary: dict
141
- compat: Dict[str, dict]
141
+ compat: Dict[str, Any]
142
142
  metadata: DeviceMetadataDataType
143
143
  isEmbedded: bool
144
144
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zwave-js-server-python
3
- Version: 0.35.0
3
+ Version: 0.35.1
4
4
  Summary: Python wrapper for zwave-js-server
5
5
  Home-page: https://github.com/home-assistant-libs/zwave-js-server-python
6
6
  Author: Home Assistant Team
@@ -1,11 +1,8 @@
1
- scripts/__init__.py,sha256=-eA0KPzIlF2qFKS5muKslF9AS1Bygp7zgmzrx9Yc0-o,22
2
- scripts/generate_multilevel_sensor_constants.py,sha256=_v-ul_JCa0GhEYjXAcANA1w9X-_9UC0LDRf-13OtGTA,8114
3
- scripts/run_mock_server.py,sha256=LQLmXH7IY4KLxkgYNXBD8wmDMsYywJ2KIl2pdnptrIA,13720
4
1
  zwave_js_server/__init__.py,sha256=Ey3O4Tha56uU-M92oLJmQHupCJ7B9oZmxlQTo8pGUM8,45
5
2
  zwave_js_server/__main__.py,sha256=1gWC927sa3FwcA47ndmrRWW0HlfYVdiUarjEsccRF2k,3624
6
3
  zwave_js_server/client.py,sha256=5t1c0xI9dokP8IOR01uMAk5oxMylAp91hTlJPkMEtRE,14827
7
4
  zwave_js_server/dump.py,sha256=hTyeW8fPleyQyYyPjFeR_JxwmFRHvtpmpn1L9uNpfiw,1213
8
- zwave_js_server/event.py,sha256=u51M7Oow5Tt3DlNlJq16_C5g_zAV3_P445qfsOAjQMA,1951
5
+ zwave_js_server/event.py,sha256=E2bIEb635al9MsCB5rkk3ZklU4-YkmqmXtifzlbxXaQ,1958
9
6
  zwave_js_server/exceptions.py,sha256=MaCynJuy2JkXA7FW_tpdpIokiHJuFoLwxktPSey5iBA,5402
10
7
  zwave_js_server/firmware.py,sha256=F5selNpOWWICuLs3L8zBjddnGKrEmip7LriijjL9pDA,878
11
8
  zwave_js_server/version.py,sha256=RDFP2kSU3cCVIQbgMWggdeDnvhtVsZzzZg9pOjH7CC8,364
@@ -29,7 +26,7 @@ zwave_js_server/model/__init__.py,sha256=XfyKH8lxZ3CB7CbRkOr5sR-eV0GrklZiBtNPmln
29
26
  zwave_js_server/model/association.py,sha256=FsgKAha7mw6dpk7u7zdQoNEajTyiL0S5eOYcY4M2Rx4,529
30
27
  zwave_js_server/model/command_class.py,sha256=pb_aXIl1_r9iHzdEyRr300y5xu62uE6GzmF6540CQd4,1256
31
28
  zwave_js_server/model/device_class.py,sha256=KpSM101bGa_3Mig4C-_rkE4CzqImK2JPIUtlG-TFcvU,1886
32
- zwave_js_server/model/device_config.py,sha256=PfwyGakw82VcET7ERlARykFDZsslRXCKYfQfkxddjI0,6655
29
+ zwave_js_server/model/device_config.py,sha256=7NaMa4BZ5tGe2p0YtOQFIUjBwRVF7GYWVExLwAgIUtE,6659
33
30
  zwave_js_server/model/driver.py,sha256=dwbLNWdzKQa50191zWMP9duZVag0Ns4FnGLi8lTwxCE,6022
34
31
  zwave_js_server/model/endpoint.py,sha256=DwthJmJ9vw6jmCFowykaAtutK-hO5qBO7EkkEcKtSFg,5153
35
32
  zwave_js_server/model/firmware.py,sha256=0AlV2mbjlmejV1eh0oHCUU-2Ysyec9a0X9BTvHG3wYs,2493
@@ -57,9 +54,9 @@ zwave_js_server/util/node.py,sha256=KUg-Z-7hdDLGKKCYLktc4oOgiPbDT7ZUYqUH_IPLwO4,
57
54
  zwave_js_server/util/command_class/__init__.py,sha256=sRxti47ekLTzfk8B609CMQumIbcD6mon2ZS0zwh9omY,59
58
55
  zwave_js_server/util/command_class/meter.py,sha256=Qkif1ul4KpV_k6Dc7yNXlSjlJLLTH8mCitgQk9J9W0A,1277
59
56
  zwave_js_server/util/command_class/multilevel_sensor.py,sha256=p92YLNRze5z3UWhfIDJO95F-j5tqx1PKUG9oamo9Dng,1446
60
- zwave_js_server_python-0.35.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
- zwave_js_server_python-0.35.0.dist-info/METADATA,sha256=4N1uE0C6DV_sjtNiC10CbmXKk4NDDIfBLl-xqIWmYkE,1712
62
- zwave_js_server_python-0.35.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
63
- zwave_js_server_python-0.35.0.dist-info/entry_points.txt,sha256=_8Swg1yhKiBElo6k1fxIEd6E8uPz8PDxQpwQoZ29FZE,74
64
- zwave_js_server_python-0.35.0.dist-info/top_level.txt,sha256=v0ZjpwAaM9UZi0qaqAo3mwTHhQM-pMf3STb_iJX2Eyc,24
65
- zwave_js_server_python-0.35.0.dist-info/RECORD,,
57
+ zwave_js_server_python-0.35.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
58
+ zwave_js_server_python-0.35.1.dist-info/METADATA,sha256=-Hgzn0O6X8Fcc3h6qWVLPnhaODmfOCSFUU4MBfv2k1U,1712
59
+ zwave_js_server_python-0.35.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
60
+ zwave_js_server_python-0.35.1.dist-info/entry_points.txt,sha256=_8Swg1yhKiBElo6k1fxIEd6E8uPz8PDxQpwQoZ29FZE,74
61
+ zwave_js_server_python-0.35.1.dist-info/top_level.txt,sha256=-hwsl-i4Av5Op_yfOHC_OP56KPmzp_iVEkeohRIN5Ng,16
62
+ zwave_js_server_python-0.35.1.dist-info/RECORD,,
scripts/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Scripts module."""
@@ -1,245 +0,0 @@
1
- """Script to generate Multilevel Sensor CC constants."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import pathlib
6
- import re
7
- import subprocess
8
- from collections import defaultdict
9
- from typing import Callable, List
10
-
11
- import requests
12
- from slugify import slugify
13
-
14
- GITHUB_PROJECT = "zwave-js/node-zwave-js"
15
- BRANCH_NAME = "master"
16
- SENSOR_TYPES_FILE_PATH = "packages/config/config/sensorTypes.json"
17
- DEFAULT_SCALES_FILE_PATH = "packages/config/config/scales.json"
18
-
19
- CONST_FILE_PATH = pathlib.Path(__file__).parent.joinpath(
20
- "../zwave_js_server/const/command_class/multilevel_sensor.py"
21
- )
22
-
23
-
24
- def remove_comments(text: str) -> str:
25
- """Remove comments from a JSON string."""
26
- return "\n".join(
27
- line for line in text.split("\n") if not line.strip().startswith("//")
28
- )
29
-
30
-
31
- def remove_paranthesis(text: str) -> str:
32
- """Remove text in paranethesis from a string."""
33
- return re.sub(r"\([^)]*\)", "", text)
34
-
35
-
36
- def enum_name_format(name: str, should_remove_paranthesis: bool) -> str:
37
- """Convert sensor/scale name to enum format."""
38
- if should_remove_paranthesis:
39
- name = remove_paranthesis(name)
40
- return slugify(name, separator="_").upper()
41
-
42
-
43
- def normalize_name(name: str) -> str:
44
- """Convert a sensor/scale name into a normalized name."""
45
- return enum_name_format(name, True).replace("_", " ").title()
46
-
47
-
48
- def format_for_class_name(name: str) -> str:
49
- """Convert sensor/scale name to class name format."""
50
- return f"{normalize_name(name).replace(' ', '')}Scale"
51
-
52
-
53
- def normalize_scale_definition(scale_definitions: dict[str, dict]) -> dict[str, int]:
54
- """Convert a scales definition dictionary into a normalized dictionary."""
55
- scale_def_ = {}
56
- for scale_id, scale_props in scale_definitions.items():
57
- scale_id = int(scale_id, 16)
58
- scale_name_ = enum_name_format(scale_props["label"], True)
59
- scale_def_[scale_name_] = scale_id
60
-
61
- return dict(sorted(scale_def_.items(), key=lambda kv: kv[0]))
62
-
63
-
64
- sensor_types = json.loads(
65
- remove_comments(
66
- requests.get(
67
- (
68
- f"https://raw.githubusercontent.com/{GITHUB_PROJECT}/{BRANCH_NAME}/"
69
- f"{SENSOR_TYPES_FILE_PATH}"
70
- )
71
- ).text
72
- )
73
- )
74
- default_scales = json.loads(
75
- remove_comments(
76
- requests.get(
77
- (
78
- f"https://raw.githubusercontent.com/{GITHUB_PROJECT}/{BRANCH_NAME}/"
79
- f"{DEFAULT_SCALES_FILE_PATH}"
80
- )
81
- ).text
82
- )
83
- )
84
-
85
- scales = {
86
- normalize_name(scale_name): normalize_scale_definition(scale_def)
87
- for scale_name, scale_def in default_scales.items()
88
- }
89
-
90
- sensors = {}
91
- for sensor_id, sensor_props in sensor_types.items():
92
- sensor_id = int(sensor_id, 16)
93
- scale_def = sensor_props["scales"]
94
- remove_paranthesis_ = True
95
- if sensor_id in (87, 88):
96
- remove_paranthesis_ = False
97
- sensor_name = enum_name_format(sensor_props["label"], remove_paranthesis_)
98
- sensors[sensor_name] = {"id": sensor_id}
99
- if isinstance(scale_def, str):
100
- sensors[sensor_name]["scale"] = normalize_name(
101
- scale_def.replace("$SCALES:", "")
102
- )
103
- else:
104
- scales[sensor_name] = normalize_scale_definition(scale_def)
105
- sensors[sensor_name]["scale"] = normalize_name(sensor_name)
106
-
107
- scales = dict(sorted(scales.items(), key=lambda kv: kv[0]))
108
- sensors = dict(sorted(sensors.items(), key=lambda kv: kv[0]))
109
-
110
-
111
- def generate_int_enum_class_definition(
112
- class_name: str,
113
- enum_dict: dict[str, str | dict],
114
- enum_ref_url: str | None = None,
115
- get_id_func: Callable | None = None,
116
- docstring_info: str = "",
117
- ) -> List[str]:
118
- """Generate an IntEnum class definition as an array of lines of string."""
119
- class_def = []
120
- class_def.append(f"class {class_name}(IntEnum):")
121
- docstring = (
122
- f'"""Enum for known {docstring_info} multilevel sensor types."""'.replace(
123
- " ", " "
124
- )
125
- )
126
- class_def.append(f" {docstring}")
127
- if enum_ref_url:
128
- class_def.append(f" # {enum_ref_url}")
129
- for enum_name, enum_id in enum_dict.items():
130
- if get_id_func:
131
- enum_id = get_id_func(enum_id)
132
- class_def.append(f" {enum_name} = {enum_id}")
133
- return class_def
134
-
135
-
136
- SENSOR_TYPE_URL = (
137
- f"https://github.com/{GITHUB_PROJECT}/blob/{BRANCH_NAME}/{SENSOR_TYPES_FILE_PATH}"
138
- )
139
-
140
- lines = [
141
- '"""Constants for the Multilevel Sensor CC."""',
142
- "",
143
- "# ----------------------------------------------------------------------------------- #",
144
- "# **BEGINNING OF AUTOGENERATED CONTENT** (TO ADD ADDITIONAL MANUAL CONTENT, LOOK FOR #",
145
- '# THE "END OF AUTOGENERATED CONTENT" COMMENT BLOCK AND ADD YOUR CODE BELOW IT) #',
146
- "# ----------------------------------------------------------------------------------- #",
147
- "",
148
- "from enum import IntEnum",
149
- "from typing import Dict, Set, Type, Union",
150
- 'CC_SPECIFIC_SCALE = "scale"',
151
- 'CC_SPECIFIC_SENSOR_TYPE = "sensorType"',
152
- ]
153
-
154
- lines.extend(
155
- generate_int_enum_class_definition(
156
- "MultilevelSensorType",
157
- sensors,
158
- SENSOR_TYPE_URL,
159
- get_id_func=lambda x: x["id"],
160
- )
161
- )
162
-
163
- unit_name_to_enum_map = defaultdict(list)
164
- for scale_name, scale_dict in scales.items():
165
- lines.extend(
166
- generate_int_enum_class_definition(
167
- format_for_class_name(scale_name),
168
- scale_dict,
169
- SENSOR_TYPE_URL,
170
- docstring_info=f"scales for {scale_name}",
171
- )
172
- )
173
- for unit_name in scale_dict.keys():
174
- unit_name_to_enum_map[unit_name].append(
175
- f"{format_for_class_name(scale_name)}.{unit_name}"
176
- )
177
- unit_name_to_enum_map = dict(
178
- sorted(unit_name_to_enum_map.items(), key=lambda kv: kv[0])
179
- )
180
- for unit_name, enum_list in unit_name_to_enum_map.items():
181
- unit_name_to_enum_map[unit_name] = sorted(enum_list)
182
-
183
- scale_class_names = [format_for_class_name(scale_name) for scale_name in scales]
184
- lines.extend(
185
- [f"MultilevelSensorScaleType = Union[{', '.join(sorted(scale_class_names))}]", ""]
186
- )
187
-
188
- multilevel_sensor_type_to_scale_map_line = (
189
- "MULTILEVEL_SENSOR_TYPE_TO_SCALE_MAP: Dict[MultilevelSensorType, "
190
- "Type[MultilevelSensorScaleType]] = {"
191
- )
192
- for sensor_name, sensor_def in sensors.items():
193
- multilevel_sensor_type_to_scale_map_line += (
194
- f" MultilevelSensorType.{sensor_name}: "
195
- f"{format_for_class_name(sensor_def['scale'])},"
196
- )
197
- multilevel_sensor_type_to_scale_map_line += "}"
198
- lines.append(multilevel_sensor_type_to_scale_map_line)
199
- lines.append("")
200
-
201
- for unit_name, unit_enums in unit_name_to_enum_map.items():
202
- lines.append(
203
- f"UNIT_{unit_name}: Set[MultilevelSensorScaleType] = {{{','.join(sorted(unit_enums))}}}"
204
- )
205
-
206
- lines.extend(
207
- [
208
- "",
209
- "# ----------------------------------------------------------------------------------- #",
210
- "# **END OF AUTOGENERATED CONTENT** (DO NOT EDIT/REMOVE THIS COMMENT BLOCK AND DO NOT #",
211
- "# EDIT ANYTHING ABOVE IT. IF A NEW IMPORT IS NEEDED, UPDATE THE LINES AROUND 135 #",
212
- "# IN scripts/generate_multilevel_sensor_constants.py THEN RE-RUN THE SCRIPT. ALL #",
213
- "# LINES WRITTEN BELOW THIS BLOCK WILL BE PRESERVED AS LONG AS THIS BLOCK REMAINS) #",
214
- "# ----------------------------------------------------------------------------------- #",
215
- "",
216
- ]
217
- )
218
-
219
- existing_const_file = CONST_FILE_PATH.read_text(encoding="utf-8").splitlines()
220
-
221
- manually_written_code_start_idx = (
222
- next(
223
- i
224
- for i, line in enumerate(existing_const_file)
225
- if "**END OF AUTOGENERATED CONTENT**" in line
226
- )
227
- + 6
228
- )
229
- if len(existing_const_file) > manually_written_code_start_idx:
230
- lines.extend(
231
- [
232
- line.strip("\n")
233
- for line in existing_const_file[manually_written_code_start_idx:]
234
- ]
235
- )
236
-
237
- CONST_FILE_PATH.write_text("\n".join(lines), encoding="utf-8")
238
-
239
- if subprocess.run(["which", "black"], capture_output=True, check=True).stdout:
240
- subprocess.run(
241
- ["black", CONST_FILE_PATH],
242
- check=True,
243
- )
244
- else:
245
- print("Could not run black on new file, please run it to properly format it.")
@@ -1,385 +0,0 @@
1
- """Run a mock zwave-js-server instance off of a network state dump."""
2
- import argparse
3
- import asyncio
4
- import json
5
- import logging
6
- from collections import defaultdict
7
- from collections.abc import Hashable
8
- from typing import Any, DefaultDict, List, Optional, Union
9
-
10
- from aiohttp import WSMsgType, web, web_request
11
-
12
- from zwave_js_server.client import SIZE_PARSE_JSON_EXECUTOR
13
- from zwave_js_server.const import MAX_SERVER_SCHEMA_VERSION, MIN_SERVER_SCHEMA_VERSION
14
- from zwave_js_server.model.version import VersionInfoDataType
15
-
16
- DATEFMT = "%Y-%m-%d %H:%M:%S"
17
- FMT = "%(asctime)s [%(levelname)s] %(message)s"
18
-
19
-
20
- class ExitException(Exception):
21
- """Represent an exit error."""
22
-
23
-
24
- # https://stackoverflow.com/a/1151686
25
- class HashableDict(dict):
26
- """Dictionary that can be used as a key in a dictionary."""
27
-
28
- def __key(self) -> tuple:
29
- return tuple((k, self[k]) for k in sorted(self))
30
-
31
- def __hash__(self) -> int: # type: ignore
32
- return hash(self.__key())
33
-
34
- def __eq__(self, other: Any) -> bool:
35
- # pylint: disable=protected-access
36
- return isinstance(other, HashableDict) and self.__key() == other.__key()
37
-
38
-
39
- class MockZwaveJsServer:
40
- """
41
- Class to represent a mock zwave-js-server instance.
42
-
43
- The last client that connected to the server will be the one that receives ws msgs.
44
- """
45
-
46
- def __init__(
47
- self,
48
- network_state_dump: List[dict],
49
- events_to_replay: List[dict],
50
- command_results: DefaultDict[HashableDict, list],
51
- ) -> None:
52
- """Initialize class."""
53
- self.network_state_dump = network_state_dump
54
- self.app = web.Application()
55
- self.app.add_routes(
56
- [
57
- web.get("/", self.server_handler),
58
- web.post("/replay", self.replay_handler),
59
- ]
60
- )
61
- self.primary_ws_resp: Optional[web.WebSocketResponse] = None
62
- self.events_to_replay = events_to_replay
63
- self.command_results = command_results
64
-
65
- async def send_json(self, data: dict) -> None:
66
- """Send JSON."""
67
- logging.debug("Sending JSON: %s", data)
68
- assert self.primary_ws_resp is not None
69
- await self.primary_ws_resp.send_json(data)
70
-
71
- async def send_command_result(
72
- self,
73
- data: dict,
74
- message_id: str,
75
- ) -> None:
76
- """Send message."""
77
- await self.send_json({**data, "messageId": message_id})
78
-
79
- async def send_success_command_result(
80
- self,
81
- result: Optional[dict],
82
- message_id: str,
83
- ) -> None:
84
- """Send success message."""
85
- if result is None:
86
- result = {}
87
- await self.send_command_result(
88
- {"result": result, "type": "result", "success": True}, message_id
89
- )
90
-
91
- async def process_record(self, record: dict) -> None:
92
- """Process a replay dump record."""
93
- if record.get("record_type") not in ("event", "command"):
94
- raise TypeError(f"Malformed record: {record}")
95
- if record["record_type"] == "event":
96
- await self.send_json(record["data"])
97
- else:
98
- add_command_result(self.command_results, record)
99
-
100
- async def server_handler(
101
- self, request: web_request.Request
102
- ) -> web.WebSocketResponse:
103
- """Handle websocket requests to the server."""
104
- ws_resp = web.WebSocketResponse(autoclose=False)
105
- self.primary_ws_resp = ws_resp
106
- await ws_resp.prepare(request)
107
-
108
- version_info: VersionInfoDataType = self.network_state_dump[0]
109
- # adjust min/max schemas if needed to get things to work
110
- if MAX_SERVER_SCHEMA_VERSION > version_info["maxSchemaVersion"]:
111
- version_info["maxSchemaVersion"] = MAX_SERVER_SCHEMA_VERSION
112
- if MIN_SERVER_SCHEMA_VERSION < version_info["minSchemaVersion"]:
113
- version_info["minSchemaVersion"] = MIN_SERVER_SCHEMA_VERSION
114
- await self.send_json(version_info)
115
-
116
- async for msg in ws_resp:
117
- if msg.type == WSMsgType.TEXT:
118
- logging.debug("Message received: %s", msg.data)
119
- if msg.data == "close":
120
- await ws_resp.close()
121
- elif msg.data == "error":
122
- logging.warning("Error from client: %s", msg.data)
123
-
124
- try:
125
- if len(msg.data) > SIZE_PARSE_JSON_EXECUTOR:
126
- data: dict = await asyncio.get_event_loop().run_in_executor(
127
- None, msg.json
128
- )
129
- else:
130
- data = msg.json()
131
- except ValueError as err:
132
- raise ExitException(f"Received invalid JSON {msg.data}") from err
133
-
134
- if "command" not in data:
135
- raise ExitException(f"Malformed message: {data}")
136
-
137
- cmd = data["command"]
138
- message_id = data["messageId"]
139
- if cmd == "set_api_schema":
140
- await self.send_json(self.network_state_dump[1])
141
- elif cmd == "driver.get_log_config":
142
- await self.send_success_command_result(
143
- {
144
- "config": {
145
- "enabled": True,
146
- "level": "silly",
147
- "logToFile": False,
148
- "nodeFilter": [],
149
- "filename": None,
150
- "forceConsole": False,
151
- }
152
- },
153
- message_id,
154
- )
155
- elif cmd == "start_listening":
156
- await self.send_json(self.network_state_dump[2])
157
- await asyncio.sleep(1)
158
- for event in self.events_to_replay:
159
- await self.send_json(event)
160
- elif resp_list := self.command_results[sanitize_msg(data)]:
161
- await self.send_command_result(resp_list.pop(0), message_id)
162
- else:
163
- raise ExitException(f"Unhandled command received: {data}")
164
- elif msg.type == WSMsgType.ERROR:
165
- logging.error(
166
- "Connection closed with exception %s",
167
- ws_resp.exception(),
168
- )
169
-
170
- logging.info("Connection closed")
171
-
172
- return ws_resp
173
-
174
- async def replay_handler(self, request: web_request.Request) -> web.Response:
175
- """Handle requests to replay dump."""
176
- try:
177
- data = await request.json()
178
- except json.decoder.JSONDecodeError:
179
- return web.Response(status=400, reason="Invalid JSON.")
180
-
181
- if isinstance(data, list):
182
- for record in data:
183
- try:
184
- await self.process_record(record)
185
- except Exception as err:
186
- return web.Response(status=400, reason=err.args[0])
187
- elif isinstance(data, dict):
188
- try:
189
- await self.process_record(data)
190
- except Exception as err:
191
- return web.Response(status=400, reason=err.args[0])
192
- else:
193
- return web.Response(status=400, reason=f"Malformed message: {data}")
194
- return web.Response(status=200)
195
-
196
-
197
- def _hashable_value(item: Union[dict, list, Hashable]) -> Union[tuple, list, Hashable]:
198
- """Return hashable value from item."""
199
- if isinstance(item, dict):
200
- return make_dict_hashable(item)
201
- if isinstance(item, list):
202
- return make_list_hashable(item)
203
- return item
204
-
205
-
206
- def make_list_hashable(lst: list) -> tuple:
207
- """Make a list hashable."""
208
- return tuple(_hashable_value(item) for item in lst)
209
-
210
-
211
- def make_dict_hashable(dct: dict) -> HashableDict:
212
- """Convert a dictionary to a hashable dictionary."""
213
- return HashableDict({key: _hashable_value(value) for key, value in dct.items()})
214
-
215
-
216
- def sanitize_msg(msg: dict) -> HashableDict:
217
- """Sanitize command message."""
218
- msg = msg.copy()
219
- msg.pop("messageId", None)
220
- return make_dict_hashable(msg)
221
-
222
-
223
- def add_command_result(
224
- command_results: DefaultDict[HashableDict, list],
225
- record: dict,
226
- ) -> None:
227
- """Add a command result to command_results map."""
228
- if "result_msg" not in record:
229
- logging.warning(
230
- "The following record cannot be used because the client did not wait for "
231
- "a response: %s",
232
- record,
233
- )
234
- return
235
- command_msg = sanitize_msg(record["command_msg"])
236
- # Response message doesn't need to be sanitized here because it will be sanitized
237
- # in the MockZwaveJsServer.send_command_result method.
238
- result_msg = record["result_msg"]
239
- command_results[command_msg].append(result_msg)
240
-
241
-
242
- def get_args() -> argparse.Namespace:
243
- """Get arguments."""
244
- parser = argparse.ArgumentParser(description="Dump Instance")
245
- parser.add_argument(
246
- "network_state_path", type=str, help="File path to network state dump JSON."
247
- )
248
- parser.add_argument("--host", type=str, help="Host to bind to", default="127.0.0.1")
249
- parser.add_argument(
250
- "--port", type=int, help="Port to run on (defaults to 3000)", default=3000
251
- )
252
- parser.add_argument(
253
- "--log-level",
254
- type=str.upper,
255
- help="Log level",
256
- default="INFO",
257
- choices=["DEBUG", "INFO", "WARNING", "ERROR"],
258
- )
259
- parser.add_argument(
260
- "--events-to-replay-path",
261
- type=str,
262
- help=(
263
- "File path to events to replay JSON. Events provided by "
264
- "--combined-replay-dump-path option will be first, followed by events "
265
- "from this file."
266
- ),
267
- default=None,
268
- )
269
- parser.add_argument(
270
- "--command-results-path",
271
- type=str,
272
- help=(
273
- "File path to command result JSON. Command results provided by "
274
- "--combined-replay-dump-path option will be first, followed by results "
275
- "from this file."
276
- ),
277
- default=None,
278
- )
279
- parser.add_argument(
280
- "--combined-replay-dump-path",
281
- type=str,
282
- help=(
283
- "File path to the combined event and command result dump JSON. Events and "
284
- "command results will be extracted in the order they were received."
285
- ),
286
- default=None,
287
- )
288
- return parser.parse_args()
289
-
290
-
291
- def main() -> None:
292
- """Run main entrypoint."""
293
- args = get_args()
294
-
295
- with open(args.network_state_path, "r", encoding="utf8") as fp:
296
- network_state_dump: List[dict] = json.load(fp)
297
-
298
- events_to_replay = []
299
- command_results: DefaultDict[HashableDict, list] = defaultdict(list)
300
-
301
- if args.combined_replay_dump_path:
302
- with open(args.combined_replay_dump_path, "r", encoding="utf8") as fp:
303
- records: List[dict] = json.load(fp)
304
-
305
- for record in records:
306
- if record.get("record_type") not in ("event", "command"):
307
- raise ExitException(
308
- f"Invalid record in combined replay dump file: {record}"
309
- )
310
- if record["record_type"] == "event":
311
- events_to_replay.append(record["data"])
312
- else:
313
- add_command_result(command_results, record)
314
-
315
- if args.events_to_replay_path:
316
- with open(args.events_to_replay_path, "r", encoding="utf8") as fp:
317
- records = json.load(fp)
318
- if (
319
- bad_record := next(
320
- (
321
- record
322
- for record in records
323
- if record.get("record_type") != "event"
324
- ),
325
- None,
326
- )
327
- ) is not None:
328
- raise ExitException(
329
- f"Malformed record in events to replay file: {bad_record}"
330
- )
331
- events_to_replay.extend([record["data"] for record in records])
332
-
333
- if args.command_results_path:
334
- with open(args.command_results_path, "r", encoding="utf8") as fp:
335
- records = json.load(fp)
336
- if (
337
- bad_record := next(
338
- (
339
- record
340
- for record in records
341
- if record.get("record_type") != "command"
342
- ),
343
- None,
344
- )
345
- ) is not None:
346
- raise ExitException(
347
- f"Malformed record in command results dump file: {bad_record}"
348
- )
349
- for record in records:
350
- add_command_result(command_results, record)
351
-
352
- # adapted from homeassistant.bootstrap.async_enable_logging
353
- logging.basicConfig(level=args.log_level)
354
- try:
355
- # pylint: disable=import-outside-toplevel
356
- from colorlog import ColoredFormatter
357
-
358
- colorfmt = f"%(log_color)s{FMT}%(reset)s"
359
- logging.getLogger().handlers[0].setFormatter(
360
- ColoredFormatter(
361
- colorfmt,
362
- datefmt=DATEFMT,
363
- log_colors={
364
- "DEBUG": "cyan",
365
- "INFO": "green",
366
- "WARNING": "yellow",
367
- "ERROR": "red",
368
- "CRITICAL": "red",
369
- },
370
- )
371
- )
372
- except ImportError:
373
- logging.getLogger().handlers[0].setFormatter(
374
- logging.Formatter(fmt=FMT, datefmt=DATEFMT)
375
- )
376
-
377
- server = MockZwaveJsServer(network_state_dump, events_to_replay, command_results)
378
- web.run_app(server.app, host=args.host, port=args.port)
379
-
380
-
381
- if __name__ == "__main__":
382
- try:
383
- main()
384
- except ExitException as error:
385
- logging.error("Fatal error: %s", error)