Skip to content

Commit 58050f2

Browse files
committed
Allowing websockets 10
More MyPy typing. Some refactoring
1 parent 5c45be5 commit 58050f2

File tree

6 files changed

+90
-69
lines changed

6 files changed

+90
-69
lines changed

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def read(filename, parent=None):
1919
setup(
2020
name="vallox_websocket_api",
2121
packages=["vallox_websocket_api"],
22-
version="2.8.1",
22+
version="2.9.0",
2323
python_requires=">=3.6.0, <4",
2424
description="Vallox WebSocket API",
2525
author="Jevgeni Kiski",
@@ -37,7 +37,7 @@ def read(filename, parent=None):
3737
"Programming Language :: Python",
3838
"Programming Language :: Python :: 3",
3939
],
40-
install_requires=["websockets >= 9.1, < 10.0", "construct >= 2.9.0, < 3.0.0",],
40+
install_requires=["websockets >= 9.1, < 11.0", "construct >= 2.9.0, < 3.0.0",],
4141
setup_requires=["wheel"],
42-
tests_require=["mock", "asynctest",],
42+
tests_require=["mock", "asynctest"],
4343
)

tests/test_client.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
from tests.decorators import with_client
88
from vallox_websocket_api.exceptions import ValloxWebsocketException
9+
from vallox_websocket_api.client import Client
910

1011

1112
class TestClient(asynctest.TestCase):
1213
@with_client
13-
async def testFetchMetric(self, client, ws):
14+
async def testFetchMetric(self, client: Client, ws):
1415
ws.recv.return_value = binascii.unhexlify(
1516
"0024000000000000000000000000000001000800030000000000000061df98b100030003203fb9500331000000000000000000560000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b000f732a6ca969a171d1730800010000022700000028000000000000000001a6029e000100000028ffffffffffffffffffffffffffffffffffffffffffffffff000000000000000057c503e8ffffffffffff000000190000000000010000000000000000000300001b98012000a50000000000000000001e00010000000100000000000000000007001b000f001700010012000200070044000000010000000000000007003200320001000000000000001e0000c0a80501ffffff0000000000000000000000000000000000000000000000000000000000c0a8050c86076097f78844b7ac4db61e502fe4f2004c004c000100c00101001c001e000a00320000003703840000708f00320032000a0000000000010000000a721f0000000000010000000f728300000000000000000064715700000000000000000000000000000000000000010037001e000000000000000068bf71bb000083910000002600b4000000010001000000010001001e000f00080001001200000003000000000000000000000017000003e90000000000000001000100010000000a003200010000000000000000000000000000000000000000001000000000000000000000000000540048000000000000000000000000000000c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
1617
)
@@ -22,39 +23,39 @@ async def testFetchMetric(self, client, ws):
2223
ws.send.assert_called_once_with(binascii.unhexlify("0300f6000000f900"))
2324

2425
@with_client
25-
async def testSetTempValue(self, client, ws):
26+
async def testSetTempValue(self, client: Client, ws):
2627
ws.recv.return_value = binascii.unhexlify("0200f500f700")
2728

2829
await client.set_values({"A_CYC_BOOST_AIR_TEMP_TARGET": "19"})
2930

3031
ws.send.assert_called_once_with(binascii.unhexlify("0400f90022501f723ec3"))
3132

3233
@with_client
33-
async def testSetTempValueFraction(self, client, ws):
34+
async def testSetTempValueFraction(self, client: Client, ws):
3435
ws.recv.return_value = binascii.unhexlify("0200f500f700")
3536

3637
await client.set_values({"A_CYC_BOOST_AIR_TEMP_TARGET": "19.1"})
3738

3839
ws.send.assert_called_once_with(binascii.unhexlify("0400f9002250297248c3"))
3940

4041
@with_client
41-
async def testSetTempValueFractionRounding1(self, client, ws):
42+
async def testSetTempValueFractionRounding1(self, client: Client, ws):
4243
ws.recv.return_value = binascii.unhexlify("0200f500f700")
4344

4445
await client.set_values({"A_CYC_BOOST_AIR_TEMP_TARGET": "19.145"})
4546

4647
ws.send.assert_called_once_with(binascii.unhexlify("0400f9002250297248c3"))
4748

4849
@with_client
49-
async def testSetTempValueFractionRounding2(self, client, ws):
50+
async def testSetTempValueFractionRounding2(self, client: Client, ws):
5051
ws.recv.return_value = binascii.unhexlify("0200f500f700")
5152

5253
await client.set_values({"A_CYC_BOOST_AIR_TEMP_TARGET": "18.991"})
5354

5455
ws.send.assert_called_once_with(binascii.unhexlify("0400f90022501f723ec3"))
5556

5657
@with_client
57-
async def testSetValue(self, client, ws):
58+
async def testSetValue(self, client: Client, ws):
5859
ws.recv.return_value = binascii.unhexlify("0200f500f700")
5960

6061
await client.set_values(
@@ -71,7 +72,7 @@ async def testSetValue(self, client, ws):
7172
)
7273

7374
@with_client
74-
async def testSetAssertion(self, client, ws):
75+
async def testSetAssertion(self, client: Client, ws):
7576
ws.recv.return_value = binascii.unhexlify("0200f500f700")
7677

7778
with self.assertRaises(AssertionError):
@@ -87,37 +88,37 @@ async def testSetAssertion(self, client, ws):
8788
await client.set_values({"A_CYC_FIREPLACE_SUPP_FAN": "11.2"})
8889

8990
@with_client
90-
async def testSetMissing(self, client, ws):
91+
async def testSetMissing(self, client: Client, ws):
9192
ws.recv.return_value = binascii.unhexlify("0200f500f700")
9293

9394
with self.assertRaises(AttributeError):
9495
await client.set_values({"A_CYC_BOOSTER": 10})
9596

9697
@with_client
97-
async def testSetUnsettable(self, client, ws):
98+
async def testSetUnsettable(self, client: Client, ws):
9899
ws.recv.return_value = binascii.unhexlify("0200f500f700")
99100

100101
with self.assertRaises(AttributeError):
101102
await client.set_values({"A_CYC_RH_VALUE": 22})
102103

103104
@with_client
104-
async def testSetNewSettableAddressByName(self, client, ws):
105+
async def testSetNewSettableAddressByName(self, client: Client, ws):
105106
ws.recv.return_value = binascii.unhexlify("0200f500f700")
106107

107108
client.set_settable_address("A_CYC_RH_VALUE", int)
108109

109110
await client.set_values({"A_CYC_RH_VALUE": 22})
110111

111112
@with_client
112-
async def testSetNewSettableAddressByAddress(self, client, ws):
113+
async def testSetNewSettableAddressByAddress(self, client: Client, ws):
113114
ws.recv.return_value = binascii.unhexlify("0200f500f700")
114115

115116
client.set_settable_address(4363, int)
116117

117118
await client.set_values({"A_CYC_RH_VALUE": 22})
118119

119120
@with_client
120-
async def testSetNewSettableAddressByAddressException(self, client, ws):
121+
async def testSetNewSettableAddressByAddressException(self, client: Client, ws):
121122
ws.recv.side_effect = CoroutineMock(side_effect=InvalidMessage())
122123

123124
client.set_settable_address(4363, int)

vallox_websocket_api/client.py

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import asyncio
12
import re
3+
from functools import wraps
4+
from typing import Dict, Union, Tuple, List, Optional, Any, TypeVar, Callable, cast
25

36
import websockets
47

@@ -11,7 +14,7 @@
1114
KPageSize = 65536
1215

1316

14-
def calculate_offset(aIndex):
17+
def calculate_offset(aIndex: int) -> int:
1518
offset = 0
1619

1720
if (aIndex > vlxDevConstants.RANGE_START_g_cyclone_general_info) and (
@@ -125,6 +128,28 @@ def to_kelvin(value: float) -> int:
125128
return int(round(value * 10) * 10 + 27315)
126129

127130

131+
FuncT = TypeVar("FuncT", bound=Callable[..., Any])
132+
133+
134+
def _websocket_exception_handler(request_fn: FuncT) -> FuncT:
135+
@wraps(request_fn)
136+
async def wrapped(*args: Any, **kwargs: Any) -> Any:
137+
try:
138+
return await request_fn(*args, **kwargs)
139+
except websockets.InvalidHandshake as e:
140+
raise ValloxWebsocketException("Websocket handshake failed") from e
141+
except websockets.InvalidURI as e:
142+
raise ValloxWebsocketException("Websocket invalid URI") from e
143+
except websockets.PayloadTooBig as e:
144+
raise ValloxWebsocketException("Websocket payload too big") from e
145+
except websockets.InvalidState as e:
146+
raise ValloxWebsocketException("Websocket invalid state") from e
147+
except websockets.WebSocketProtocolError as e:
148+
raise ValloxWebsocketException("Websocket protocol error") from e
149+
150+
return cast(FuncT, wrapped)
151+
152+
128153
class Client:
129154
SETTABLE_INT_VALS = {
130155
re.compile("^A_CYC_STATE$"),
@@ -135,18 +160,12 @@ class Client:
135160
re.compile("^A_CYC_(?:EXTR|SUPP)_FAN_BALANCE_BASE$"),
136161
}
137162

138-
_settable_addresses = None
163+
_settable_addresses: Dict[int, type]
139164

140-
def get_settable_addresses(self):
141-
if not self._settable_addresses:
142-
self._settable_addresses = dict(
143-
(v, int)
144-
for k, v in vlxDevConstants.__dict__.items()
145-
if any(r.match(k) for r in self.SETTABLE_INT_VALS)
146-
)
165+
def get_settable_addresses(self) -> Dict[int, type]:
147166
return self._settable_addresses
148167

149-
def set_settable_address(self, address, var_type):
168+
def set_settable_address(self, address: Union[int, str], var_type: type) -> None:
150169
if var_type not in [int, float]:
151170
raise AttributeError("Only float or int type are supported")
152171

@@ -165,16 +184,23 @@ def set_settable_address(self, address, var_type):
165184
"Unable to add address '%s' to settable list" % str(address)
166185
)
167186

168-
def __init__(self, ip_address):
187+
def __init__(self, ip_address: str):
169188
self.ip_address = ip_address
170189

171-
def _decode_pair(self, key, value):
190+
self._settable_addresses = dict(
191+
(v, int)
192+
for k, v in vlxDevConstants.__dict__.items()
193+
if any(r.match(k) for r in self.SETTABLE_INT_VALS)
194+
)
195+
196+
def _decode_pair(self, key: str, value: Union[int,str]) -> Tuple[int, Union[int, float]]:
172197
try:
173198
address = int(getattr(vlxDevConstants, key, key))
174199
except ValueError:
175200
raise AttributeError("%s setting does not exist" % key)
176201
if "_TEMP_" in key:
177202
value = to_kelvin(float(value))
203+
raw_value: Union[int, float]
178204
try:
179205
raw_value = int(value)
180206
except ValueError:
@@ -185,42 +211,35 @@ def _decode_pair(self, key, value):
185211
required_type = addresses[address]
186212
except KeyError:
187213
raise AttributeError("%s setting is not settable" % key)
214+
188215
assert type(raw_value) == required_type, (
189216
"%s(%d) key needs to be an %s, but %s passed"
190217
% (key, address, required_type.__name__, type(raw_value).__name__)
191218
)
192219

193220
return address, raw_value
194221

195-
async def _websocket_request(self, payload, read_packets=1):
196-
try:
197-
async with websockets.connect("ws://%s/" % self.ip_address) as ws:
198-
await ws.send(payload)
199-
results = []
200-
for i in range(0, read_packets):
201-
r = await ws.recv()
202-
203-
results.append(r)
204-
return results[0] if read_packets == 1 else results
205-
except websockets.InvalidHandshake as e:
206-
raise ValloxWebsocketException("Websocket handshake failed") from e
207-
except websockets.InvalidURI as e:
208-
raise ValloxWebsocketException("Websocket invalid URI") from e
209-
except websockets.PayloadTooBig as e:
210-
raise ValloxWebsocketException("Websocket payload too big") from e
211-
except websockets.InvalidState as e:
212-
raise ValloxWebsocketException("Websocket invalid state") from e
213-
except websockets.WebSocketProtocolError as e:
214-
raise ValloxWebsocketException("Websocket protocol error") from e
222+
@_websocket_exception_handler
223+
async def _websocket_request(self, payload: bytes) -> bytes:
224+
async with websockets.connect("ws://%s/" % self.ip_address) as ws:
225+
await ws.send(payload)
226+
r: bytes = await ws.recv()
227+
return r
228+
229+
@_websocket_exception_handler
230+
async def _websocket_request_multiple(self, payload: bytes, read_packets: int) -> List[bytes]:
231+
async with websockets.connect("ws://%s/" % self.ip_address) as ws:
232+
await ws.send(payload)
233+
return await asyncio.gather(*[ws.recv() for _ in range(0, read_packets)])
215234

216-
async def fetch_metrics(self, metric_keys=None):
235+
async def fetch_metrics(self, metric_keys: Optional[List[str]] = None) -> Dict[str, Union[int, float]]:
217236
metrics = {}
218237
payload = ReadTableRequest.build({})
219238
result = await self._websocket_request(payload)
220239

221240
data = ReadTableResponse.parse(result)
222241

223-
if not metric_keys:
242+
if metric_keys is None:
224243
metric_keys = vlxDevConstants.__dict__.keys()
225244

226245
for key in metric_keys:
@@ -233,9 +252,9 @@ async def fetch_metrics(self, metric_keys=None):
233252

234253
return metrics
235254

236-
async def fetch_raw_logs(self):
255+
async def fetch_raw_logs(self) -> List[List[Dict[str, Union[int, float]]]]:
237256
payload = LogReadRequest.build({})
238-
result = await self._websocket_request(payload, read_packets=2)
257+
result = await self._websocket_request_multiple(payload, read_packets=2)
239258
page_count = LogReadResponse1.parse(result[0]).fields.value.pages
240259

241260
expected_total_len = KPageSize * page_count
@@ -269,10 +288,10 @@ async def fetch_raw_logs(self):
269288

270289
return series
271290

272-
async def fetch_metric(self, metric_key):
291+
async def fetch_metric(self, metric_key: str) -> Optional[Union[int, float]]:
273292
return (await self.fetch_metrics([metric_key])).get(metric_key, None)
274293

275-
async def set_values(self, dict_):
294+
async def set_values(self, dict_: Dict[str, Union[int,str]]) -> bool:
276295
items = []
277296
for key, value in dict_.items():
278297
address, raw_value = self._decode_pair(key, value)

vallox_websocket_api/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
class Constants(object):
2-
def __setattr__(self, key, value):
2+
def __setattr__(self, key: str, value: int) -> None:
33
super(Constants, self).__setattr__(key, value)
44

55

vallox_websocket_api/messages.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .constants import vlxDevConstants
88

99

10-
def checksum_16(data):
10+
def checksum_16(data: bytes) -> int:
1111
c = 0
1212
for i in range(len(data) // 2):
1313
c = c + (data[i * 2 + 1] << 8) + data[i * 2]
@@ -72,7 +72,7 @@ def checksum_16(data):
7272

7373

7474
class DateAdapter(Adapter):
75-
def _decode(self, obj, context, path):
75+
def _decode(self, obj, context, path) -> datetime.datetime:
7676
return datetime.datetime(
7777
year=2000 + obj[4], month=obj[3], day=obj[2], hour=obj[1], minute=obj[0]
7878
)

0 commit comments

Comments
 (0)