ramses-rf 0.22.40__py3-none-any.whl → 0.51.2__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.
Files changed (71) hide show
  1. ramses_cli/__init__.py +18 -0
  2. ramses_cli/client.py +597 -0
  3. ramses_cli/debug.py +20 -0
  4. ramses_cli/discovery.py +405 -0
  5. ramses_cli/utils/cat_slow.py +17 -0
  6. ramses_cli/utils/convert.py +60 -0
  7. ramses_rf/__init__.py +31 -10
  8. ramses_rf/binding_fsm.py +787 -0
  9. ramses_rf/const.py +124 -105
  10. ramses_rf/database.py +297 -0
  11. ramses_rf/device/__init__.py +69 -39
  12. ramses_rf/device/base.py +187 -376
  13. ramses_rf/device/heat.py +540 -552
  14. ramses_rf/device/hvac.py +279 -171
  15. ramses_rf/dispatcher.py +153 -177
  16. ramses_rf/entity_base.py +478 -361
  17. ramses_rf/exceptions.py +82 -0
  18. ramses_rf/gateway.py +377 -513
  19. ramses_rf/helpers.py +57 -19
  20. ramses_rf/py.typed +0 -0
  21. ramses_rf/schemas.py +148 -194
  22. ramses_rf/system/__init__.py +16 -23
  23. ramses_rf/system/faultlog.py +363 -0
  24. ramses_rf/system/heat.py +295 -302
  25. ramses_rf/system/schedule.py +312 -198
  26. ramses_rf/system/zones.py +318 -238
  27. ramses_rf/version.py +2 -8
  28. ramses_rf-0.51.2.dist-info/METADATA +72 -0
  29. ramses_rf-0.51.2.dist-info/RECORD +55 -0
  30. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info}/WHEEL +1 -2
  31. ramses_rf-0.51.2.dist-info/entry_points.txt +2 -0
  32. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info/licenses}/LICENSE +1 -1
  33. ramses_tx/__init__.py +160 -0
  34. {ramses_rf/protocol → ramses_tx}/address.py +65 -59
  35. ramses_tx/command.py +1454 -0
  36. ramses_tx/const.py +903 -0
  37. ramses_tx/exceptions.py +92 -0
  38. {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
  39. {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
  40. ramses_tx/gateway.py +338 -0
  41. ramses_tx/helpers.py +883 -0
  42. {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
  43. {ramses_rf/protocol → ramses_tx}/message.py +155 -191
  44. ramses_tx/opentherm.py +1260 -0
  45. ramses_tx/packet.py +210 -0
  46. {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
  47. ramses_tx/protocol.py +801 -0
  48. ramses_tx/protocol_fsm.py +672 -0
  49. ramses_tx/py.typed +0 -0
  50. {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
  51. {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
  52. ramses_tx/transport.py +1471 -0
  53. ramses_tx/typed_dicts.py +492 -0
  54. ramses_tx/typing.py +181 -0
  55. ramses_tx/version.py +4 -0
  56. ramses_rf/discovery.py +0 -398
  57. ramses_rf/protocol/__init__.py +0 -59
  58. ramses_rf/protocol/backports.py +0 -42
  59. ramses_rf/protocol/command.py +0 -1576
  60. ramses_rf/protocol/const.py +0 -697
  61. ramses_rf/protocol/exceptions.py +0 -111
  62. ramses_rf/protocol/helpers.py +0 -390
  63. ramses_rf/protocol/opentherm.py +0 -1170
  64. ramses_rf/protocol/packet.py +0 -235
  65. ramses_rf/protocol/protocol.py +0 -613
  66. ramses_rf/protocol/transport.py +0 -1011
  67. ramses_rf/protocol/version.py +0 -10
  68. ramses_rf/system/hvac.py +0 -82
  69. ramses_rf-0.22.40.dist-info/METADATA +0 -64
  70. ramses_rf-0.22.40.dist-info/RECORD +0 -42
  71. ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
@@ -1,613 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
- """RAMSES RF - RAMSES-II compatible Message processor.
5
-
6
- Operates at the msg layer of: app - msg - pkt - h/w
7
- """
8
- from __future__ import annotations
9
-
10
- import asyncio
11
- import logging
12
- import signal
13
- from datetime import datetime as dt
14
- from datetime import timedelta as td
15
- from queue import Empty, Full, PriorityQueue, SimpleQueue
16
- from typing import Awaitable, Callable, Dict, Iterable, List, Optional, TypeVar
17
-
18
- from .command import Command
19
- from .const import SZ_DAEMON, SZ_EXPIRED, SZ_EXPIRES, SZ_FUNC, SZ_TIMEOUT, __dev_mode__
20
- from .exceptions import CorruptStateError, InvalidPacketError
21
- from .message import Message
22
- from .packet import Packet
23
-
24
- _MessageProtocolT = TypeVar("_MessageProtocolT", bound="MessageProtocol")
25
- _MessageTransportT = TypeVar("_MessageTransportT", bound="MessageTransport")
26
-
27
-
28
- DONT_CREATE_MESSAGES = 3 # duplicate
29
-
30
- SZ_WRITER_TASK = "writer_task"
31
-
32
- DEV_MODE = __dev_mode__ and False
33
-
34
- _LOGGER = logging.getLogger(__name__)
35
- # _LOGGER.setLevel(logging.WARNING)
36
- if DEV_MODE:
37
- _LOGGER.setLevel(logging.DEBUG)
38
-
39
-
40
- class CallbackAsAwaitable:
41
- """Create an pair of functions so that the callback can be awaited.
42
-
43
- The awaitable (getter) starts its `timeout` timer only when it is invoked.
44
- It may raise a `TimeoutError` or a `TypeError`.
45
-
46
- The callback (putter) may put the message in the queue before the getter is invoked.
47
- """
48
-
49
- SAFETY_TIMEOUT_DEFAULT = 9.9 # used to prevent waiting forever
50
- SAFETY_TIMEOUT_MINIMUM = 5.0
51
- HAS_TIMED_OUT = False
52
- SHORT_WAIT = 0.001 # seconds
53
-
54
- def __init__(self, loop) -> None:
55
- self._loop = loop or asyncio.get_event_loop()
56
- self._queue: SimpleQueue = SimpleQueue() # unbounded, but we use only 1 entry
57
-
58
- self.expires: dt = None # type: ignore[assignment]
59
-
60
- # the awaitable...
61
- async def getter(self, timeout: float = SAFETY_TIMEOUT_DEFAULT) -> Message:
62
- """Poll the queue until the message arrives, or the timer expires."""
63
-
64
- if timeout <= self.SAFETY_TIMEOUT_MINIMUM:
65
- timeout = self.SAFETY_TIMEOUT_MINIMUM
66
- self.expires = dt.now() + td(seconds=timeout)
67
-
68
- while dt.now() < self.expires:
69
- try:
70
- msg = self._queue.get_nowait()
71
- break
72
- except Empty:
73
- await asyncio.sleep(self.SHORT_WAIT)
74
- else:
75
- raise TimeoutError(f"Safety timer expired (timeout={timeout}s)")
76
-
77
- if msg is self.HAS_TIMED_OUT:
78
- raise TimeoutError("Command timer expired")
79
- if not isinstance(msg, Message):
80
- raise TypeError(f"Response is not a message: {msg}")
81
- return msg
82
-
83
- # the callback...
84
- def putter(self, msg: Message) -> None:
85
- """Put the message in the queue (when invoked)."""
86
-
87
- self._queue.put_nowait(msg)
88
-
89
-
90
- def awaitable_callback(loop) -> tuple[Callable[..., Awaitable[Message]], Callable]:
91
- """Create a pair of functions, so that a callback can be awaited."""
92
- obj = CallbackAsAwaitable(loop)
93
- return obj.getter, obj.putter # awaitable, callback
94
-
95
-
96
- class MessageTransport(asyncio.Transport):
97
- """Interface for a message transport.
98
-
99
- There may be several implementations, but typically, the user does not implement
100
- new transports; rather, the platform provides some useful transports that are
101
- implemented using the platform's best practices.
102
-
103
- The user never instantiates a transport directly; they call a utility function,
104
- passing it a protocol factory and other information necessary to create the
105
- transport and protocol. (E.g. EventLoop.create_connection() or
106
- EventLoop.create_server().)
107
-
108
- The utility function will asynchronously create a transport and a protocol and
109
- hook them up by calling the protocol's connection_made() method, passing it the
110
- transport.
111
- """
112
-
113
- MAX_BUFFER_SIZE = 200
114
- MAX_SUBSCRIBERS = 3
115
-
116
- READER = "receiver_callback"
117
- WRITER = SZ_WRITER_TASK
118
-
119
- _extra: dict # asyncio.BaseTransport
120
-
121
- def __init__(self, gwy, protocol: MessageProtocol, extra: dict = None) -> None:
122
- super().__init__(extra=extra)
123
-
124
- self._loop = gwy._loop
125
-
126
- self._gwy = gwy
127
- self._protocols: List[MessageProtocol] = []
128
- self._extra[self.READER] = self._pkt_receiver
129
- self._dispatcher: Callable = None # type: ignore[assignment]
130
-
131
- self._callbacks: Dict[str, dict] = {}
132
-
133
- self._que: PriorityQueue = PriorityQueue(maxsize=self.MAX_BUFFER_SIZE)
134
- self._write_buffer_limit_high: int = self.MAX_BUFFER_SIZE
135
- self._write_buffer_limit_low: int = 0
136
- self._write_buffer_paused = False
137
- self.set_write_buffer_limits()
138
-
139
- # self._extra[self.WRITER] = self._loop.create_task(self._polling_loop())
140
-
141
- for sig in (signal.SIGINT, signal.SIGTERM):
142
- self._loop.add_signal_handler(sig, self.abort)
143
-
144
- self._is_closing = False
145
-
146
- self.add_protocol(protocol) # calls protocol.commection_made()
147
-
148
- def _set_dispatcher(self, dispatcher: Callable) -> None:
149
- _LOGGER.debug("MsgTransport._set_dispatcher(%s)", dispatcher)
150
-
151
- async def call_send_data(cmd):
152
- _LOGGER.debug("MsgTransport.pkt_dispatcher(%s): send_data", cmd)
153
- if cmd._cbk:
154
- self._add_callback(cmd.rx_header or cmd.tx_header, cmd._cbk, cmd=cmd)
155
-
156
- if _LOGGER.getEffectiveLevel() == logging.INFO: # i.e. don't log for DEBUG
157
- _LOGGER.info("SENT: %s", cmd)
158
-
159
- await self._dispatcher(cmd) # send_data, *once* callback registered
160
-
161
- async def pkt_dispatcher():
162
- """Poll the queue and send any command packets to the lower layer."""
163
- while True:
164
- try:
165
- cmd = self._que.get_nowait()
166
- except Empty:
167
- if self._is_closing:
168
- break
169
- await asyncio.sleep(0.05)
170
- continue
171
- except AttributeError: # when self._que == None, from abort()
172
- break
173
-
174
- try:
175
- if self._dispatcher:
176
- await call_send_data(cmd)
177
- except (AssertionError, NotImplementedError): # TODO: needs checking
178
- pass
179
- # except:
180
- # _LOGGER.exception("")
181
- # continue
182
-
183
- self._que.task_done()
184
- self.get_write_buffer_size()
185
-
186
- _LOGGER.error("MsgTransport.pkt_dispatcher(): connection_lost(None)")
187
- [p.connection_lost(None) for p in self._protocols]
188
-
189
- self._dispatcher = dispatcher # type: ignore[assignment]
190
- self._extra[self.WRITER] = self._loop.create_task(pkt_dispatcher())
191
-
192
- return self._extra[self.WRITER]
193
-
194
- def _add_callback(
195
- self, header: str, callback: dict, cmd: None | Command = None
196
- ) -> None:
197
- # assert header not in self._callbacks # CBK, below
198
- # self._callbacks[header] = CallbackWrapper(header, callback, cmd=cmd)
199
-
200
- callback[SZ_EXPIRES] = (
201
- dt.max
202
- if callback.get(SZ_DAEMON)
203
- else dt.now() + td(seconds=callback.get(SZ_TIMEOUT, 1))
204
- )
205
- callback["cmd"] = cmd
206
- self._callbacks[header] = callback
207
-
208
- def _pkt_receiver(self, pkt: Packet) -> None:
209
- _LOGGER.debug("MsgTransport._pkt_receiver(%s)", pkt)
210
-
211
- if _LOGGER.getEffectiveLevel() == logging.INFO: # i.e. don't log for DEBUG
212
- _LOGGER.info("rcvd: %s", pkt)
213
-
214
- # HACK: 1st, notify all expired callbacks
215
- for hdr, callback in self._callbacks.items():
216
- if not callback.get(SZ_EXPIRED) and (
217
- callback.get(SZ_EXPIRES, dt.max) < pkt.dtm
218
- ):
219
- # see also: PktProtocolQos.send_data()
220
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(
221
- "MsgTransport._pkt_receiver(%s): Expired callback", hdr
222
- )
223
- callback[SZ_FUNC](CallbackAsAwaitable.HAS_TIMED_OUT) # ZX: 1/3
224
- callback[SZ_EXPIRED] = not callback.get(SZ_DAEMON, False) # HACK:
225
-
226
- # HACK: 2nd, discard any expired callbacks
227
- self._callbacks = {
228
- hdr: callback
229
- for hdr, callback in self._callbacks.items()
230
- if callback.get(SZ_DAEMON)
231
- or (callback[SZ_EXPIRES] >= pkt.dtm and not callback.get(SZ_EXPIRED))
232
- }
233
-
234
- if len(self._protocols) == 0 or (
235
- self._gwy.config.reduce_processing >= DONT_CREATE_MESSAGES
236
- ):
237
- return
238
-
239
- # BUG: all InvalidPacketErrors are not being raised here (see below)
240
- try:
241
- msg = Message(self._gwy, pkt) # should log all invalid msgs appropriately
242
- except InvalidPacketError:
243
- return
244
-
245
- # HACK: 3rd, invoke any callback
246
- # NOTE: msg._pkt._hdr is expensive - don't call it unless there's callbacks
247
- if self._callbacks and (callback := self._callbacks.get(msg._pkt._hdr)): # type: ignore[assignment]
248
- callback[SZ_FUNC](msg) # ZX: 2/3
249
- if not callback.get(SZ_DAEMON):
250
- del self._callbacks[msg._pkt._hdr]
251
-
252
- # BUG: the InvalidPacketErrors here should have been caught above
253
- # BUG: should only need to catch CorruptStateError
254
- for p in self._protocols:
255
- try:
256
- self._loop.call_soon(p.data_received, msg)
257
-
258
- except InvalidPacketError:
259
- return
260
-
261
- except CorruptStateError as exc:
262
- _LOGGER.error("%s < %s", pkt, exc)
263
-
264
- except ( # protect this code from the upper-layer callback
265
- AssertionError,
266
- ) as exc:
267
- if p is not self._protocols[0]:
268
- raise
269
- _LOGGER.error("%s < exception from app layer: %s", pkt, exc)
270
-
271
- except ( # protect this code from the upper-layer callback
272
- ArithmeticError, # incl. ZeroDivisionError,
273
- AttributeError,
274
- LookupError, # incl. IndexError, KeyError
275
- NameError, # incl. UnboundLocalError
276
- RuntimeError, # incl. RecursionError
277
- TypeError,
278
- ValueError,
279
- ) as exc:
280
- if p is self._protocols[0]:
281
- raise
282
- _LOGGER.error("%s < exception from app layer: %s", pkt, exc)
283
-
284
- def get_extra_info(self, name: str, default=None):
285
- """Get optional transport information."""
286
-
287
- return self._extra.get(name, default)
288
-
289
- def abort(self) -> None:
290
- """Close the transport immediately.
291
-
292
- Buffered data will be lost. No more data will be received. The protocol's
293
- connection_lost() method will (eventually) be called with None as its argument.
294
- """
295
-
296
- self._is_closing = True
297
- self._clear_write_buffer()
298
- self.close()
299
-
300
- def close(self) -> None:
301
- """Close the transport.
302
-
303
- Buffered data will be flushed asynchronously. No more data will be received.
304
- After all buffered data is flushed, the protocol's connection_lost() method will
305
- (eventually) be called with None as its argument.
306
- """
307
-
308
- if self._is_closing:
309
- return
310
- self._is_closing = True
311
-
312
- self._pause_protocols()
313
- if task := self._extra.get(self.WRITER):
314
- task.cancel()
315
-
316
- [self._loop.call_soon(p.connection_lost, None) for p in self._protocols]
317
-
318
- def is_closing(self) -> bool:
319
- """Return True if the transport is closing or closed."""
320
- return self._is_closing
321
-
322
- def add_protocol(self, protocol: MessageProtocol) -> None:
323
- """Attach a new protocol.
324
-
325
- Allow multiple protocols per transport.
326
- """
327
-
328
- if protocol not in self._protocols:
329
- if len(self._protocols) > self.MAX_SUBSCRIBERS - 1:
330
- raise ValueError("Exceeded maximum number of subscribing protocols")
331
-
332
- self._protocols.append(protocol)
333
- protocol.connection_made(self)
334
-
335
- def get_protocols(self) -> list:
336
- """Return the list of active protocols.
337
-
338
- There can be multiple protocols per transport.
339
- """
340
-
341
- return self._protocols
342
-
343
- def is_reading(self) -> bool:
344
- """Return True if the transport is receiving new data."""
345
-
346
- raise NotImplementedError
347
-
348
- def pause_reading(self) -> None:
349
- """Pause the receiving end.
350
-
351
- No data will be passed to the protocol's data_received() method until
352
- resume_reading() is called.
353
- """
354
-
355
- raise NotImplementedError
356
-
357
- def resume_reading(self) -> None:
358
- """Resume the receiving end.
359
-
360
- Data received will once again be passed to the protocol's data_received()
361
- method.
362
- """
363
-
364
- raise NotImplementedError
365
-
366
- def _clear_write_buffer(self) -> None:
367
- """Empty the dispatch queue.
368
-
369
- Should not call `get_write_buffer_size()`.
370
- """
371
-
372
- while not self._que.empty():
373
- try:
374
- self._que.get_nowait()
375
- except Empty:
376
- continue
377
- self._que.task_done()
378
-
379
- def _pause_protocols(self, force: bool = None) -> None:
380
- """Pause the other end."""
381
-
382
- if not self._write_buffer_paused or force:
383
- self._write_buffer_paused = True
384
- for p in self._protocols:
385
- p.pause_writing()
386
-
387
- def _resume_protocols(self, force: bool = None) -> None:
388
- """Resume the other end."""
389
-
390
- if self._write_buffer_paused or force:
391
- self._write_buffer_paused = False
392
- for p in self._protocols:
393
- p.resume_writing()
394
-
395
- def get_write_buffer_limits(self) -> tuple[int, int]:
396
- """Get the high and low watermarks for write flow control.
397
-
398
- Return a tuple (low, high) where low and high are positive number of bytes.
399
- """
400
-
401
- return self._write_buffer_limit_low, self._write_buffer_limit_high
402
-
403
- def set_write_buffer_limits(self, high: int = None, low: int = None) -> None:
404
- """Set the high- and low-water limits for write flow control.
405
-
406
- These two values control when to call the protocol's pause_writing() and
407
- resume_writing() methods. If specified, the low-water limit must be less than
408
- or equal to the high-water limit. Neither value can be negative. The defaults
409
- are implementation-specific. If only the high-water limit is given, the
410
- low-water limit defaults to an implementation-specific value less than or equal
411
- to the high-water limit. Setting high to zero forces low to zero as well, and
412
- causes pause_writing() to be called whenever the buffer becomes non-empty.
413
- Setting low to zero causes resume_writing() to be called only once the buffer is
414
- empty. Use of zero for either limit is generally sub-optimal as it reduces
415
- opportunities for doing I/O and computation concurrently.
416
- """
417
-
418
- high = self.MAX_BUFFER_SIZE if high is None else high
419
- low = int(self._write_buffer_limit_high * 0.8) if low is None else low
420
-
421
- self._write_buffer_limit_high = max((min((high, self.MAX_BUFFER_SIZE)), 0))
422
- self._write_buffer_limit_low = min((max((low, 0)), high))
423
-
424
- self.get_write_buffer_size()
425
-
426
- def get_write_buffer_size(self) -> int:
427
- """Return the current size of the write buffer.
428
-
429
- If required, pause or resume the protocols.
430
- """
431
-
432
- qsize = self._que.qsize()
433
-
434
- if qsize >= self._write_buffer_limit_high:
435
- self._pause_protocols()
436
-
437
- elif qsize <= self._write_buffer_limit_low:
438
- self._resume_protocols()
439
-
440
- return qsize
441
-
442
- def write(self, cmd: Command) -> None:
443
- """Write some data bytes to the transport.
444
-
445
- This does not block; it buffers the data and arranges for it to be sent out
446
- asynchronously.
447
- """
448
- _LOGGER.debug("MsgTransport.write(%s)", cmd)
449
-
450
- if self._is_closing:
451
- raise RuntimeError("MsgTransport is closing or has closed")
452
-
453
- if self._write_buffer_paused:
454
- raise RuntimeError("MsgTransport write buffer is paused")
455
-
456
- if self._gwy.config.disable_sending:
457
- raise RuntimeError("MsgTransport sending is disabled (cmd discarded)")
458
-
459
- else:
460
- # if not self._dispatcher: # TODO: do better?
461
- # _LOGGER.debug("MsgTransport.write(%s): no dispatcher", cmd)
462
-
463
- try:
464
- self._que.put_nowait(cmd)
465
- except Full:
466
- pass # TODO: why? - consider restarting the dispatcher
467
-
468
- self.get_write_buffer_size()
469
-
470
- def writelines(self, list_of_cmds: Iterable[Command]) -> None:
471
- """Write a list (or any iterable) of data bytes to the transport.
472
-
473
- The default implementation concatenates the arguments and calls write() on the
474
- result.list_of_cmds
475
- """
476
-
477
- for cmd in list_of_cmds:
478
- self.write(cmd)
479
-
480
- def write_eof(self) -> None:
481
- """Close the write end after flushing buffered data.
482
-
483
- This is like typing ^D into a UNIX program reading from stdin. Data may still be
484
- received.
485
- """
486
-
487
- raise NotImplementedError
488
-
489
- def can_write_eof(self) -> bool:
490
- """Return True if this transport supports write_eof(), False if not."""
491
-
492
- return False
493
-
494
-
495
- class MessageProtocol(asyncio.Protocol):
496
- """Interface for a message protocol.
497
-
498
- The user should implement this interface. They can inherit from this class but
499
- don't need to. The implementations here do nothing (they don't raise
500
- exceptions).
501
-
502
- When the user wants to requests a transport, they pass a protocol factory to a
503
- utility function (e.g., EventLoop.create_connection()).
504
-
505
- When the connection is made successfully, connection_made() is called with a
506
- suitable transport object. Then data_received() will be called 0 or more times
507
- with data (bytes) received from the transport; finally, connection_lost() will
508
- be called exactly once with either an exception object or None as an argument.
509
-
510
- State machine of calls:
511
-
512
- start -> CM [-> DR*] [-> ER?] -> CL -> end
513
-
514
- * CM: connection_made()
515
- * DR: data_received()
516
- * ER: eof_received()
517
- * CL: connection_lost()
518
- """
519
-
520
- def __init__(self, gwy, callback: Callable) -> None:
521
-
522
- # self._gwy = gwy # is not used
523
- self._loop = gwy._loop
524
- self._callback = callback
525
-
526
- self._transport: MessageTransport = None # type: ignore[assignment]
527
- self._prev_msg: None | Message = None
528
- self._this_msg: None | Message = None
529
-
530
- self._pause_writing = True
531
-
532
- def connection_made(self, transport: MessageTransport) -> None: # type: ignore[override]
533
- """Called when a connection is made."""
534
- self._transport = transport
535
- self.resume_writing()
536
-
537
- def data_received(self, msg: Message) -> None: # type: ignore[override]
538
- """Called by the transport when a message is received."""
539
- _LOGGER.debug("MsgProtocol.data_received(%s)", msg)
540
-
541
- self._this_msg, self._prev_msg = msg, self._this_msg
542
- self._callback(self._this_msg, prev_msg=self._prev_msg)
543
-
544
- async def send_data(
545
- self, cmd: Command, callback: Callable = None, _make_awaitable: bool = None
546
- ) -> Optional[Message]:
547
- """Called when a command is to be sent."""
548
- _LOGGER.debug("MsgProtocol.send_data(%s)", cmd)
549
-
550
- if _make_awaitable and callback is not None:
551
- raise ValueError("only one of `awaitable` and `callback` can be provided")
552
-
553
- if _make_awaitable: # and callback is None:
554
- awaitable, callback = awaitable_callback(self._loop) # ZX: 3/3
555
- if callback: # func, args, daemon, timeout (& expired)
556
- cmd._cbk = {SZ_FUNC: callback, SZ_TIMEOUT: 3}
557
-
558
- while self._pause_writing:
559
- await asyncio.sleep(0.005)
560
-
561
- self._transport.write(cmd)
562
-
563
- if _make_awaitable:
564
- return await awaitable() # CallbackAsAwaitable.getter(timeout: float = ...)
565
- return None
566
-
567
- def connection_lost(self, exc: Optional[Exception]) -> None:
568
- """Called when the connection is lost or closed."""
569
- if exc is not None:
570
- raise exc
571
-
572
- def pause_writing(self) -> None:
573
- """Called by the transport when its buffer goes over the high-water mark."""
574
- self._pause_writing = True
575
-
576
- def resume_writing(self) -> None:
577
- """Called by the transport when its buffer drains below the low-water mark."""
578
- self._pause_writing = False
579
-
580
-
581
- def create_protocol_factory(
582
- protocol_class: type[asyncio.Protocol], *args, **kwargs
583
- ) -> Callable:
584
- def _protocol_factory() -> asyncio.Protocol:
585
- return protocol_class(*args, **kwargs)
586
-
587
- return _protocol_factory
588
-
589
-
590
- def create_msg_stack(
591
- gwy,
592
- msg_callback: Callable[[Message, Optional[Message]], None],
593
- /,
594
- *,
595
- protocol_factory: Callable[[], _MessageProtocolT] = None,
596
- ) -> tuple[_MessageProtocolT, _MessageTransportT]:
597
- """Utility function to provide a transport to a client protocol.
598
-
599
- The architecture is: app (client) -> msg -> pkt -> ser (HW interface).
600
- """
601
-
602
- def _protocol_factory():
603
- return create_protocol_factory(MessageProtocol, gwy, msg_callback)()
604
-
605
- msg_protocol = protocol_factory() if protocol_factory else _protocol_factory()
606
-
607
- if gwy.msg_transport: # TODO: a little messy?
608
- msg_transport = gwy.msg_transport
609
- msg_transport.add_protocol(msg_protocol)
610
- else:
611
- msg_transport = MessageTransport(gwy, msg_protocol)
612
-
613
- return (msg_protocol, msg_transport)