|
1 | 1 | """Stick Test Program.""" |
2 | 2 |
|
| 3 | +import asyncio |
| 4 | +from collections.abc import Callable |
3 | 5 | from datetime import UTC, datetime, timedelta |
4 | 6 | import importlib |
| 7 | +import logging |
| 8 | +import random |
| 9 | + |
| 10 | +import crcmod |
| 11 | + |
| 12 | +crc_fun = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0x0000, xorOut=0x0000) |
| 13 | + |
| 14 | + |
| 15 | +_LOGGER = logging.getLogger(__name__) |
| 16 | +_LOGGER.setLevel(logging.DEBUG) |
5 | 17 |
|
6 | 18 | pw_constants = importlib.import_module("plugwise_usb.constants") |
7 | 19 |
|
|
12 | 24 | # generate energy log timestamps with fixed hour timestamp used in tests |
13 | 25 | hour_timestamp = utc_now.replace(minute=0, second=0, microsecond=0) |
14 | 26 |
|
| 27 | + |
| 28 | +def construct_message(data: bytes, seq_id: bytes = b"0000") -> bytes: |
| 29 | + """Construct plugwise message.""" |
| 30 | + body = data[:4] + seq_id + data[4:] |
| 31 | + return bytes( |
| 32 | + pw_constants.MESSAGE_HEADER |
| 33 | + + body |
| 34 | + + bytes(f"{crc_fun(body):04X}", pw_constants.UTF8) |
| 35 | + + pw_constants.MESSAGE_FOOTER |
| 36 | + ) |
| 37 | + |
| 38 | + |
| 39 | +class DummyTransport: |
| 40 | + """Dummy transport class.""" |
| 41 | + |
| 42 | + protocol_data_received: Callable[[bytes], None] |
| 43 | + |
| 44 | + def __init__( |
| 45 | + self, |
| 46 | + loop: asyncio.AbstractEventLoop, |
| 47 | + test_data: dict[bytes, tuple[str, bytes, bytes | None]] | None = None, |
| 48 | + ) -> None: |
| 49 | + """Initialize dummy transport class.""" |
| 50 | + self._loop = loop |
| 51 | + self._msg = 0 |
| 52 | + self._seq_id = b"1233" |
| 53 | + self._processed: list[bytes] = [] |
| 54 | + self._first_response = test_data |
| 55 | + self._second_response = test_data |
| 56 | + if test_data is None: |
| 57 | + self._first_response = RESPONSE_MESSAGES |
| 58 | + self._second_response = SECOND_RESPONSE_MESSAGES |
| 59 | + self.random_extra_byte = 0 |
| 60 | + self._closing = False |
| 61 | + |
| 62 | + def inc_seq_id(self, seq_id: bytes | None) -> bytes: |
| 63 | + """Increment sequence id.""" |
| 64 | + if seq_id is None: |
| 65 | + return b"0000" |
| 66 | + temp_int = int(seq_id, 16) + 1 |
| 67 | + if temp_int >= 65532: |
| 68 | + temp_int = 0 |
| 69 | + temp_str = str(hex(temp_int)).lstrip("0x").upper() |
| 70 | + while len(temp_str) < 4: |
| 71 | + temp_str = "0" + temp_str |
| 72 | + return temp_str.encode() |
| 73 | + |
| 74 | + def is_closing(self) -> bool: |
| 75 | + """Close connection.""" |
| 76 | + return self._closing |
| 77 | + |
| 78 | + def write(self, data: bytes) -> None: |
| 79 | + """Write data back to system.""" |
| 80 | + log = None |
| 81 | + ack = None |
| 82 | + response = None |
| 83 | + if data in self._processed and self._second_response is not None: |
| 84 | + log, ack, response = self._second_response.get(data, (None, None, None)) |
| 85 | + if log is None and self._first_response is not None: |
| 86 | + log, ack, response = self._first_response.get(data, (None, None, None)) |
| 87 | + if log is None: |
| 88 | + resp = PARTLY_RESPONSE_MESSAGES.get(data[:24], (None, None, None)) |
| 89 | + if resp is None: |
| 90 | + _LOGGER.debug("No msg response for %s", str(data)) |
| 91 | + return |
| 92 | + log, ack, response = resp |
| 93 | + if ack is None: |
| 94 | + _LOGGER.debug("No ack response for %s", str(data)) |
| 95 | + return |
| 96 | + |
| 97 | + self._seq_id = self.inc_seq_id(self._seq_id) |
| 98 | + if response and self._msg == 0: |
| 99 | + self.message_response_at_once(ack, response, self._seq_id) |
| 100 | + self._processed.append(data) |
| 101 | + else: |
| 102 | + self.message_response(ack, self._seq_id) |
| 103 | + self._processed.append(data) |
| 104 | + if response is None or self._closing: |
| 105 | + return |
| 106 | + self._loop.create_task(self._delayed_response(response, self._seq_id)) |
| 107 | + self._msg += 1 |
| 108 | + |
| 109 | + async def _delayed_response(self, data: bytes, seq_id: bytes) -> None: |
| 110 | + delay = random.uniform(0.005, 0.025) |
| 111 | + await asyncio.sleep(delay) |
| 112 | + self.message_response(data, seq_id) |
| 113 | + |
| 114 | + def message_response(self, data: bytes, seq_id: bytes) -> None: |
| 115 | + """Handle message response.""" |
| 116 | + self.random_extra_byte += 1 |
| 117 | + if self.random_extra_byte > 25: |
| 118 | + self.protocol_data_received(b"\x83") |
| 119 | + self.random_extra_byte = 0 |
| 120 | + self.protocol_data_received(construct_message(data, seq_id) + b"\x83") |
| 121 | + else: |
| 122 | + self.protocol_data_received(construct_message(data, seq_id)) |
| 123 | + |
| 124 | + def message_response_at_once(self, ack: bytes, data: bytes, seq_id: bytes) -> None: |
| 125 | + """Full message.""" |
| 126 | + self.random_extra_byte += 1 |
| 127 | + if self.random_extra_byte > 25: |
| 128 | + self.protocol_data_received(b"\x83") |
| 129 | + self.random_extra_byte = 0 |
| 130 | + self.protocol_data_received( |
| 131 | + construct_message(ack, seq_id) |
| 132 | + + construct_message(data, seq_id) |
| 133 | + + b"\x83" |
| 134 | + ) |
| 135 | + else: |
| 136 | + self.protocol_data_received( |
| 137 | + construct_message(ack, seq_id) + construct_message(data, seq_id) |
| 138 | + ) |
| 139 | + |
| 140 | + def close(self) -> None: |
| 141 | + """Close connection.""" |
| 142 | + self._closing = True |
| 143 | + |
| 144 | + |
15 | 145 | LOG_TIMESTAMPS = {} |
16 | 146 | _one_hour = timedelta(hours=1) |
17 | 147 | for x in range(168): |
|
0 commit comments