diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index f51ae3d..0817aa5 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -29,6 +29,12 @@ jobs: runs-on: ubuntu-latest steps: + - name: Set timezone + uses: szenius/set-timezone@v2.0 + with: + timezoneLinux: "Europe/Berlin" + timezoneMacos: "Europe/Berlin" + timezoneWindows: "Europe/Berlin" - uses: actions/checkout@v4 - name: Set up Python 3.12 uses: actions/setup-python@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d92b5..15422a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- add timestamp to MQTT topics [#138](https://github.com/s-allius/tsun-gen3-proxy/issues/138) +- improve the message handling, to avoid hangs +- GEN3: allow long timeouts until we received first inverter data (not only device data) - bump aiomqtt to version 2.2.0 - bump schema to version 0.7.7 - Home Assistant: improve inverter status value texts diff --git a/app/build.sh b/app/build.sh index 31478d7..0fc60de 100755 --- a/app/build.sh +++ b/app/build.sh @@ -32,31 +32,44 @@ fi echo version: $VERSION build-date: $BUILD_DATE image: $IMAGE if [[ $1 == debug ]];then docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --build-arg "LOG_LVL=DEBUG" --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:debug app +echo " => pushing ${IMAGE}:debug" +docker push -q ${IMAGE}:debug + elif [[ $1 == dev ]];then docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app +echo " => pushing ${IMAGE}:dev" +docker push -q ${IMAGE}:dev elif [[ $1 == preview ]];then docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:preview -t ${IMAGE}:${VERSION} app echo 'login to ghcr.io' echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin -docker push -q ghcr.io/s-allius/tsun-gen3-proxy:preview -docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION} +echo " => pushing ${IMAGE}:preview" +docker push -q ${IMAGE}:preview +echo " => pushing ${IMAGE}:${VERSION}" +docker push -q ${IMAGE}:${VERSION} elif [[ $1 == rc ]];then docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app echo 'login to ghcr.io' echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin -docker push -q ghcr.io/s-allius/tsun-gen3-proxy:rc -docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION} +echo " => pushing ${IMAGE}:rc" +docker push -q ${IMAGE}:rc +echo " => pushing ${IMAGE}:${VERSION}" +docker push -q ${IMAGE}:${VERSION} elif [[ $1 == rel ]];then docker build --no-cache --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:latest -t ${IMAGE}:${MAJOR} -t ${IMAGE}:${VERSION} app echo 'login to ghcr.io' echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin -docker push -q ghcr.io/s-allius/tsun-gen3-proxy:latest -docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${MAJOR} -docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION} +echo " => pushing ${IMAGE}:latest" +docker push -q ${IMAGE}:latest +echo " => pushing ${IMAGE}:${MAJOR}" +docker push -q ${IMAGE}:${MAJOR} +echo " => pushing ${IMAGE}:${VERSION}" +docker push -q ${IMAGE}:${VERSION} fi -echo 'check docker-compose.yaml file' -docker-compose config -q \ No newline at end of file +echo ' => checking docker-compose.yaml file' +docker-compose config -q +echo 'done' diff --git a/app/src/async_stream.py b/app/src/async_stream.py index 1c76cb3..968b8fd 100644 --- a/app/src/async_stream.py +++ b/app/src/async_stream.py @@ -35,7 +35,7 @@ class AsyncStream(): self.proc_max = 0 def __timeout(self) -> int: - if self.state == State.init: + if self.state == State.init or self.state == State.received: to = self.MAX_START_TIME else: if self.server_side and self.modbus_polling: diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index fbc8622..e045655 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -1,7 +1,9 @@ import struct import logging import time +import pytz from datetime import datetime +from tzlocal import get_localzone if __name__ == "app.src.gen3.talent": from app.src.messages import hex_dump_memory, Message, State @@ -127,37 +129,43 @@ class Talent(Message): self.unique_id = serial_no def read(self) -> float: + '''process all received messages in the _recv_buffer''' self._read() + while True: + if not self.header_valid: + self.__parse_header(self._recv_buffer, len(self._recv_buffer)) - if not self.header_valid: - self.__parse_header(self._recv_buffer, len(self._recv_buffer)) + if self.header_valid and \ + len(self._recv_buffer) >= (self.header_len + self.data_len): + if self.state == State.init: + self.state = State.received # received 1st package - if self.header_valid and len(self._recv_buffer) >= (self.header_len + - self.data_len): - if self.state == State.init: - self.state = State.received # received 1st package + log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING) + if callable(log_lvl): + log_lvl = log_lvl() - log_lvl = self.log_lvl.get(self.msg_id, logging.WARNING) - if callable(log_lvl): - log_lvl = log_lvl() + hex_dump_memory(log_lvl, f'Received from {self.addr}:' + f' BufLen: {len(self._recv_buffer)}' + f' HdrLen: {self.header_len}' + f' DtaLen: {self.data_len}', + self._recv_buffer, len(self._recv_buffer)) - hex_dump_memory(log_lvl, f'Received from {self.addr}:', - self._recv_buffer, self.header_len+self.data_len) + self.__set_serial_no(self.id_str.decode("utf-8")) + self.__dispatch_msg() + self.__flush_recv_msg() + else: + return 0 # don not wait before sending a response - self.__set_serial_no(self.id_str.decode("utf-8")) - self.__dispatch_msg() - self.__flush_recv_msg() - return 0.5 # wait 500ms before sending a response - - def forward(self, buffer, buflen) -> None: + def forward(self) -> None: + '''add the actual receive msg to the forwarding queue''' tsun = Config.get('tsun') if tsun['enabled']: - self._forward_buffer = buffer[:buflen] + buffer = self._recv_buffer + buflen = self.header_len+self.data_len + self._forward_buffer += buffer[:buflen] hex_dump_memory(logging.DEBUG, 'Store for forwarding:', buffer, buflen) - self.__parse_header(self._forward_buffer, - len(self._forward_buffer)) fnc = self.switch.get(self.msg_id, self.msg_unknown) logger.info(self.__flow_str(self.server_side, 'forwrd') + f' Ctl: {int(self.ctrl):#02x} Msg: {fnc.__name__!r}') @@ -232,6 +240,8 @@ class Talent(Message): return switch.get(type, '???') def _timestamp(self): # pragma: no cover + '''returns timestamp fo the inverter as localtime + since 1.1.1970 in msec''' if False: # utc as epoche ts = time.time() @@ -240,23 +250,37 @@ class Talent(Message): ts = (datetime.now() - datetime(1970, 1, 1)).total_seconds() return round(ts*1000) + def _utcfromts(self, ts: float): + '''converts inverter timestamp into unix time (epoche)''' + dt = datetime.fromtimestamp(ts/1000, pytz.UTC). \ + replace(tzinfo=get_localzone()) + return dt.timestamp() + + def _utc(self): # pragma: no cover + '''returns unix time (epoche)''' + return datetime.now().timestamp() + def _update_header(self, _forward_buffer): '''update header for message before forwarding, add time offset to timestamp''' _len = len(_forward_buffer) - result = struct.unpack_from('!lB', _forward_buffer, 0) - id_len = result[1] # len of variable id string - if _len < 2*id_len + 21: - return + ofs = 0 + while ofs < _len: + result = struct.unpack_from('!lB', _forward_buffer, 0) + msg_len = 4 + result[0] + id_len = result[1] # len of variable id string + if _len < 2*id_len + 21: + return - result = struct.unpack_from('!B', _forward_buffer, id_len+6) - msg_code = result[0] - if msg_code == 0x71 or msg_code == 0x04: - result = struct.unpack_from('!q', _forward_buffer, 13+2*id_len) - ts = result[0] + self.ts_offset - logger.debug(f'offset: {self.ts_offset:08x}' - f' proxy-time: {ts:08x}') - struct.pack_into('!q', _forward_buffer, 13+2*id_len, ts) + result = struct.unpack_from('!B', _forward_buffer, id_len+6) + msg_code = result[0] + if msg_code == 0x71 or msg_code == 0x04: + result = struct.unpack_from('!q', _forward_buffer, 13+2*id_len) + ts = result[0] + self.ts_offset + logger.debug(f'offset: {self.ts_offset:08x}' + f' proxy-time: {ts:08x}') + struct.pack_into('!q', _forward_buffer, 13+2*id_len, ts) + ofs += msg_len # check if there is a complete header in the buffer, parse it # and set @@ -335,12 +359,12 @@ class Talent(Message): elif self.await_conn_resp_cnt > 0: self.await_conn_resp_cnt -= 1 else: - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() return else: logger.warning('Unknown Ctrl') self.inc_counter('Unknown_Ctrl') - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() def __process_contact_info(self) -> bool: result = struct.unpack_from('!B', self._recv_buffer, self.header_len) @@ -362,7 +386,8 @@ class Talent(Message): def msg_get_time(self): if self.ctrl.is_ind(): if self.data_len == 0: - self.state = State.pend # block MODBUS cmds + if self.state == State.up: + self.state = State.pend # block MODBUS cmds if (self.modbus_polling): self.mb_timer.start(self.mb_start_timeout) self.db.set_db_def_value(Register.POLLING_INTERVAL, @@ -387,7 +412,7 @@ class Talent(Message): logger.warning('Unknown Ctrl') self.inc_counter('Unknown_Ctrl') - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() def parse_msg_header(self): result = struct.unpack_from('!lB', self._recv_buffer, self.header_len) @@ -401,11 +426,12 @@ class Talent(Message): result = struct.unpack_from(f'!{id_len+1}pBq', self._recv_buffer, self.header_len + 4) + timestamp = result[2] logger.debug(f'ID: {result[0]} B: {result[1]}') - logger.debug(f'time: {result[2]:08x}') + logger.debug(f'time: {timestamp:08x}') # logger.info(f'time: {datetime.utcfromtimestamp(result[2]).strftime( # "%Y-%m-%d %H:%M:%S")}') - return msg_hdr_len + return msg_hdr_len, timestamp def msg_collector_data(self): if self.ctrl.is_ind(): @@ -420,7 +446,7 @@ class Talent(Message): logger.warning('Unknown Ctrl') self.inc_counter('Unknown_Ctrl') - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() def msg_inverter_data(self): if self.ctrl.is_ind(): @@ -436,14 +462,15 @@ class Talent(Message): logger.warning('Unknown Ctrl') self.inc_counter('Unknown_Ctrl') - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() def __process_data(self): - msg_hdr_len = self.parse_msg_header() + msg_hdr_len, ts = self.parse_msg_header() for key, update in self.db.parse(self._recv_buffer, self.header_len + msg_hdr_len, self.node_id): if update: + self._set_mqtt_timestamp(key, self._utcfromts(ts)) self.new_data[key] = True def msg_ota_update(self): @@ -454,7 +481,7 @@ class Talent(Message): else: logger.warning('Unknown Ctrl') self.inc_counter('Unknown_Ctrl') - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() def parse_modbus_header(self): @@ -499,17 +526,18 @@ class Talent(Message): hdr_len:], self.node_id): if update: + self._set_mqtt_timestamp(key, self._utc()) self.new_data[key] = True self.modbus_elms += 1 # count for unit tests else: logger.warning('Unknown Ctrl') self.inc_counter('Unknown_Ctrl') - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() def msg_forward(self): - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() def msg_unknown(self): logger.warning(f"Unknow Msg: ID:{self.msg_id}") self.inc_counter('Unknown_Msg') - self.forward(self._recv_buffer, self.header_len+self.data_len) + self.forward() diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index 284b843..711da66 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -219,39 +219,41 @@ class SolarmanV5(Message): self.unique_id = serial_no def read(self) -> float: + '''process all received messages in the _recv_buffer''' self._read() + while True: + if not self.header_valid: + self.__parse_header(self._recv_buffer, len(self._recv_buffer)) - if not self.header_valid: - self.__parse_header(self._recv_buffer, len(self._recv_buffer)) + if self.header_valid and len(self._recv_buffer) >= \ + (self.header_len + self.data_len+2): + log_lvl = self.log_lvl.get(self.control, logging.WARNING) + if callable(log_lvl): + log_lvl = log_lvl() + hex_dump_memory(log_lvl, f'Received from {self.addr}:', + self._recv_buffer, self.header_len + + self.data_len+2) + if self.__trailer_is_ok(self._recv_buffer, self.header_len + + self.data_len + 2): + if self.state == State.init: + self.state = State.received - if self.header_valid and len(self._recv_buffer) >= (self.header_len + - self.data_len+2): - log_lvl = self.log_lvl.get(self.control, logging.WARNING) - if callable(log_lvl): - log_lvl = log_lvl() - hex_dump_memory(log_lvl, f'Received from {self.addr}:', - self._recv_buffer, self.header_len+self.data_len+2) - if self.__trailer_is_ok(self._recv_buffer, self.header_len - + self.data_len + 2): - if self.state == State.init: - self.state = State.received - - self.__set_serial_no(self.snr) - self.__dispatch_msg() - self.__flush_recv_msg() - return 0 # wait 0s before sending a response + self.__set_serial_no(self.snr) + self.__dispatch_msg() + self.__flush_recv_msg() + else: + return 0 # wait 0s before sending a response def forward(self, buffer, buflen) -> None: + '''add the actual receive msg to the forwarding queue''' if self.no_forwarding: return tsun = Config.get('solarman') if tsun['enabled']: - self._forward_buffer = buffer[:buflen] + self._forward_buffer += buffer[:buflen] hex_dump_memory(logging.DEBUG, 'Store for forwarding:', buffer, buflen) - self.__parse_header(self._forward_buffer, - len(self._forward_buffer)) fnc = self.switch.get(self.control, self.msg_unknown) logger.info(self.__flow_str(self.server_side, 'forwrd') + f' Ctl: {int(self.control):#04x}' @@ -259,12 +261,6 @@ class SolarmanV5(Message): return def _init_new_client_conn(self) -> bool: - # self.__build_header(0x91) - # self._send_buffer += struct.pack(f'!{len(contact_name)+1}p' - # f'{len(contact_mail)+1}p', - # contact_name, contact_mail) - - # self.__finish_send_msg() return False ''' @@ -366,13 +362,17 @@ class SolarmanV5(Message): '''update header for message before forwarding, set sequence and checksum''' _len = len(_forward_buffer) - struct.pack_into(' None: fnc = self.switch.get(self.control, self.msg_unknown) @@ -481,7 +481,7 @@ class SolarmanV5(Message): logger.info(f'Model: {Model}') self.db.set_db_def_value(Register.EQUIPMENT_MODEL, Model) - def __process_data(self, ftype): + def __process_data(self, ftype, ts): inv_update = False msg_type = self.control >> 8 for key, update in self.db.parse(self._recv_buffer, msg_type, ftype, @@ -489,6 +489,7 @@ class SolarmanV5(Message): if update: if key == 'inverter': inv_update = True + self._set_mqtt_timestamp(key, ts) self.new_data[key] = True if inv_update: @@ -505,16 +506,18 @@ class SolarmanV5(Message): data = self._recv_buffer[self.header_len:] result = struct.unpack_from('= len(data): + if j >= data_len: break line += '%02x ' % abs(data[j]) line += ' ' * (3 * 16 + 9 - len(line)) + ' | ' for j in range(n-16, n): - if j >= len(data): + if j >= data_len: break c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.' line += '%c' % c @@ -105,6 +105,22 @@ class Message(metaclass=IterRegistry): '''callback for updating the header of the forward buffer''' return # pragma: no cover + def _set_mqtt_timestamp(self, key, ts: float | None): + if type(ts) is not None and \ + key not in self.new_data or \ + not self.new_data[key]: + if key == 'grid': + info_id = Register.TS_GRID + elif key == 'input': + info_id = Register.TS_INPUT + elif key == 'total': + info_id = Register.TS_TOTAL + else: + return + # tstr = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(ts)) + # logger.info(f'update: key: {key} ts:{tstr}' + self.db.set_db_def_value(info_id, round(ts)) + ''' Our puplic methods ''' diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index a928ca3..1b8b870 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -60,6 +60,7 @@ class MemoryStream(SolarmanV5): self.at_acl = {'mqtt': {'allow': ['AT+'], 'block': ['AT+WEBU']}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE', 'AT+TIME'], 'block': ['AT+WEBU']}} self.key = '' self.data = '' + self.msg_recvd = [] def _timestamp(self): return timestamp @@ -70,6 +71,7 @@ class MemoryStream(SolarmanV5): def append_msg(self, msg): self.__msg += msg self.__msg_len += len(msg) + self.__chunk_idx = 0 def publish_mqtt(self, key, data): self.key = key @@ -104,6 +106,13 @@ class MemoryStream(SolarmanV5): return c def _SolarmanV5__flush_recv_msg(self) -> None: + self.msg_recvd.append( + { + 'control': self.control, + 'seq': str(self.seq), + 'data_len': self.data_len + } + ) super()._SolarmanV5__flush_recv_msg() self.msg_count += 1 return @@ -697,29 +706,20 @@ def test_invalid_stop_byte2(InvalidStopByte, DeviceIndMsg): # only the first message must be discarded m = MemoryStream(InvalidStopByte, (0,)) m.append_msg(DeviceIndMsg) - m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since start byte is wrong - assert m.msg_count == 1 # msg flush was called - assert m.header_len==11 - assert m.snr == 2070233889 - assert m.unique_id == 0 - assert m.control == 0x4110 - assert str(m.seq) == '01:00' - assert m.data_len == 0xd4 - assert m._recv_buffer==DeviceIndMsg - assert m._send_buffer==b'' - assert m._forward_buffer==b'' - assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1 m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert m.msg_count == 2 assert m.header_len==11 assert m.snr == 2070233889 + assert m.msg_recvd[0]['control']==0x4110 + assert m.msg_recvd[0]['seq']=='01:00' + assert m.msg_recvd[0]['data_len']==0xd4 + assert m.msg_recvd[1]['control']==0x4110 + assert m.msg_recvd[1]['seq']=='01:00' + assert m.msg_recvd[1]['data_len']==0xd4 + assert m.unique_id == None - assert m.control == 0x4110 - assert str(m.seq) == '01:00' - assert m.data_len == 0xd4 assert m._recv_buffer==b'' assert m._send_buffer==b'' assert m._forward_buffer==b'' @@ -753,19 +753,6 @@ def test_invalid_checksum(InvalidChecksum, DeviceIndMsg): # only the first message must be discarded m = MemoryStream(InvalidChecksum, (0,)) m.append_msg(DeviceIndMsg) - m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since start byte is wrong - assert m.msg_count == 1 # msg flush was called - assert m.header_len==11 - assert m.snr == 2070233889 - assert m.unique_id == 0 - assert m.control == 0x4110 - assert str(m.seq) == '01:00' - assert m.data_len == 0xd4 - assert m._recv_buffer==DeviceIndMsg - assert m._send_buffer==b'' - assert m._forward_buffer==b'' - assert m.db.stat['proxy']['Invalid_Msg_Format'] == 1 m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed @@ -773,9 +760,12 @@ def test_invalid_checksum(InvalidChecksum, DeviceIndMsg): assert m.header_len==11 assert m.snr == 2070233889 assert m.unique_id == None - assert m.control == 0x4110 - assert str(m.seq) == '01:00' - assert m.data_len == 0xd4 + assert m.msg_recvd[0]['control']==0x4110 + assert m.msg_recvd[0]['seq']=='01:00' + assert m.msg_recvd[0]['data_len']==0xd4 + assert m.msg_recvd[1]['control']==0x4110 + assert m.msg_recvd[1]['seq']=='01:00' + assert m.msg_recvd[1]['data_len']==0xd4 assert m._recv_buffer==b'' assert m._send_buffer==b'' assert m._forward_buffer==b'' @@ -788,28 +778,17 @@ def test_read_message_twice(ConfigNoTsunInv1, DeviceIndMsg, DeviceRspMsg): m.append_msg(DeviceIndMsg) m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed - assert m.msg_count == 1 - assert m.header_len==11 - assert m.snr == 2070233889 - assert m.unique_id == '2070233889' - assert m.control == 0x4110 - assert str(m.seq) == '01:01' - assert m.data_len == 0xd4 - assert m._send_buffer==DeviceRspMsg - assert m._forward_buffer==b'' - assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 - - m._send_buffer = bytearray(0) # clear send buffer for next test - m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert m.msg_count == 2 assert m.header_len==11 assert m.snr == 2070233889 assert m.unique_id == '2070233889' - assert m.control == 0x4110 - assert str(m.seq) == '01:01' - assert m.data_len == 0xd4 - assert m._send_buffer==DeviceRspMsg + assert m.msg_recvd[0]['control']==0x4110 + assert m.msg_recvd[0]['seq']=='01:01' + assert m.msg_recvd[0]['data_len']==0xd4 + assert m.msg_recvd[1]['control']==0x4110 + assert m.msg_recvd[1]['seq']=='01:01' + assert m.msg_recvd[1]['data_len']==0xd4 + assert m._send_buffer==DeviceRspMsg+DeviceRspMsg assert m._forward_buffer==b'' assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 m.close() @@ -864,27 +843,8 @@ def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, Inver ConfigTsunAllowAll m = MemoryStream(DeviceIndMsg, (0,)) m.append_msg(InverterIndMsg) - m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since msg was handled and buffer flushed - assert m.msg_count == 1 - assert m.header_len==11 - assert m.snr == 2070233889 - assert m.unique_id == '2070233889' - assert m.control == 0x4110 - assert str(m.seq) == '01:01' - assert m.data_len == 0xd4 - assert m.msg_count == 1 - assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 - assert m._forward_buffer==DeviceIndMsg - assert m._send_buffer==DeviceRspMsg - - m._send_buffer = bytearray(0) # clear send buffer for next test + m._init_new_client_conn() - assert m._send_buffer==b'' - assert m._recv_buffer==InverterIndMsg - - m._send_buffer = bytearray(0) # clear send buffer for next test - m._forward_buffer = bytearray(0) # clear forward buffer for next test m.read() # read complete msg, and dispatch msg assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 assert not m.header_valid # must be invalid, since msg was handled and buffer flushed @@ -892,12 +852,14 @@ def test_read_two_messages(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, Inver assert m.header_len==11 assert m.snr == 2070233889 assert m.unique_id == '2070233889' - assert m.control == 0x4210 - assert str(m.seq) == '02:02' - assert m.data_len == 0x199 - assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 - assert m._forward_buffer==InverterIndMsg - assert m._send_buffer==InverterRspMsg + assert m.msg_recvd[0]['control']==0x4110 + assert m.msg_recvd[0]['seq']=='01:01' + assert m.msg_recvd[0]['data_len']==0xd4 + assert m.msg_recvd[1]['control']==0x4210 + assert m.msg_recvd[1]['seq']=='02:02' + assert m.msg_recvd[1]['data_len']==0x199 + assert m._forward_buffer==DeviceIndMsg+InverterIndMsg + assert m._send_buffer==DeviceRspMsg+InverterRspMsg m._send_buffer = bytearray(0) # clear send buffer for next test m._init_new_client_conn() @@ -909,41 +871,21 @@ def test_read_two_messages2(ConfigTsunAllowAll, InverterIndMsg, InverterIndMsg_8 m = MemoryStream(InverterIndMsg, (0,)) m.append_msg(InverterIndMsg_81) m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since msg was handled and buffer flushed - assert m.msg_count == 1 - assert m.header_len==11 - assert m.snr == 2070233889 - assert m.unique_id == '2070233889' - assert m.control == 0x4210 - assert m.time_ofs == 0x33e447a0 - assert str(m.seq) == '02:02' - assert m.data_len == 0x199 - assert m.msg_count == 1 - assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 - assert m._forward_buffer==InverterIndMsg - assert m._send_buffer==InverterRspMsg - - m._send_buffer = bytearray(0) # clear send buffer for next test - m._init_new_client_conn() - assert m._send_buffer==b'' - assert m._recv_buffer==InverterIndMsg_81 - - m._send_buffer = bytearray(0) # clear send buffer for next test - m._forward_buffer = bytearray(0) # clear forward buffer for next test - m.read() # read complete msg, and dispatch msg assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert m.msg_count == 2 assert m.header_len==11 assert m.snr == 2070233889 assert m.unique_id == '2070233889' - assert m.control == 0x4210 + assert m.msg_recvd[0]['control']==0x4210 + assert m.msg_recvd[0]['seq']=='02:02' + assert m.msg_recvd[0]['data_len']==0x199 + assert m.msg_recvd[1]['control']==0x4210 + assert m.msg_recvd[1]['seq']=='03:03' + assert m.msg_recvd[1]['data_len']==0x199 assert m.time_ofs == 0x33e447a0 - assert str(m.seq) == '03:03' - assert m.data_len == 0x199 - assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 - assert m._forward_buffer==InverterIndMsg_81 - assert m._send_buffer==InverterRspMsg_81 + assert m._forward_buffer==InverterIndMsg+InverterIndMsg_81 + assert m._send_buffer==InverterRspMsg+InverterRspMsg_81 m._send_buffer = bytearray(0) # clear send buffer for next test m._init_new_client_conn() @@ -1254,26 +1196,28 @@ def test_proxy_counter(): async def test_msg_build_modbus_req(ConfigTsunInv1, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, MsgModbusCmd): ConfigTsunInv1 m = MemoryStream(DeviceIndMsg, (0,), True) - m.append_msg(InverterIndMsg) m.read() assert m.control == 0x4110 assert str(m.seq) == '01:01' - assert m._recv_buffer==InverterIndMsg # unhandled next message assert m._send_buffer==DeviceRspMsg assert m._forward_buffer==DeviceIndMsg m._send_buffer = bytearray(0) # clear send buffer for next test m._forward_buffer = bytearray(0) # clear send buffer for next test await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG) - assert m._recv_buffer==InverterIndMsg # unhandled next message assert 0 == m.send_msg_ofs assert m._forward_buffer == b'' assert m.writer.sent_pdu == b'' # modbus command must be ignore, cause connection is still not up assert m._send_buffer == b'' # modbus command must be ignore, cause connection is still not up + m.append_msg(InverterIndMsg) m.read() assert m.control == 0x4210 assert str(m.seq) == '02:02' + assert m.msg_recvd[0]['control']==0x4110 + assert m.msg_recvd[0]['seq']=='01:01' + assert m.msg_recvd[1]['control']==0x4210 + assert m.msg_recvd[1]['seq']=='02:02' assert m._recv_buffer==b'' assert m._send_buffer==InverterRspMsg assert m._forward_buffer==InverterIndMsg @@ -1298,36 +1242,31 @@ async def test_msg_build_modbus_req(ConfigTsunInv1, DeviceIndMsg, DeviceRspMsg, async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, AtCommandIndMsg, AtCommandRspMsg): ConfigTsunAllowAll m = MemoryStream(DeviceIndMsg, (0,), True) - m.append_msg(InverterIndMsg) - m.append_msg(AtCommandRspMsg) m.read() # read device ind assert m.control == 0x4110 assert str(m.seq) == '01:01' - assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # unhandled next message assert m._send_buffer==DeviceRspMsg assert m._forward_buffer==DeviceIndMsg m._send_buffer = bytearray(0) # clear send buffer for next test m._forward_buffer = bytearray(0) # clear send buffer for next test await m.send_at_cmd('AT+TIME=214028,1,60,120') - assert m._recv_buffer==InverterIndMsg + AtCommandRspMsg # unhandled next message assert m._send_buffer==b'' assert m._forward_buffer==b'' assert str(m.seq) == '01:01' assert m.mqtt.key == '' assert m.mqtt.data == "" + m.append_msg(InverterIndMsg) m.read() # read inverter ind assert m.control == 0x4210 assert str(m.seq) == '02:02' - assert m._recv_buffer==AtCommandRspMsg # unhandled next message assert m._send_buffer==InverterRspMsg assert m._forward_buffer==InverterIndMsg m._send_buffer = bytearray(0) # clear send buffer for next test m._forward_buffer = bytearray(0) # clear send buffer for next test await m.send_at_cmd('AT+TIME=214028,1,60,120') - assert m._recv_buffer==AtCommandRspMsg # unhandled next message assert m._send_buffer==AtCommandIndMsg assert m._forward_buffer==b'' assert str(m.seq) == '02:03' @@ -1335,6 +1274,7 @@ async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIn assert m.mqtt.data == "" m._send_buffer = bytearray(0) # clear send buffer for next test + m.append_msg(AtCommandRspMsg) m.read() # read at resp assert m.control == 0x1510 assert str(m.seq) == '03:03' @@ -1359,24 +1299,22 @@ async def test_AT_cmd(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIn async def test_AT_cmd_blocked(ConfigTsunAllowAll, DeviceIndMsg, DeviceRspMsg, InverterIndMsg, InverterRspMsg, AtCommandIndMsg): ConfigTsunAllowAll m = MemoryStream(DeviceIndMsg, (0,), True) - m.append_msg(InverterIndMsg) m.read() assert m.control == 0x4110 assert str(m.seq) == '01:01' - assert m._recv_buffer==InverterIndMsg # unhandled next message assert m._send_buffer==DeviceRspMsg assert m._forward_buffer==DeviceIndMsg m._send_buffer = bytearray(0) # clear send buffer for next test m._forward_buffer = bytearray(0) # clear send buffer for next test await m.send_at_cmd('AT+WEBU') - assert m._recv_buffer==InverterIndMsg # unhandled next message assert m._send_buffer==b'' assert m._forward_buffer==b'' assert str(m.seq) == '01:01' assert m.mqtt.key == '' assert m.mqtt.data == "" + m.append_msg(InverterIndMsg) m.read() assert m.control == 0x4210 assert str(m.seq) == '02:02' @@ -1583,7 +1521,6 @@ def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusRsp): '''Modbus response with a valid Modbus request must be forwarded''' ConfigTsunInv1 m = MemoryStream(MsgModbusRsp) - m.append_msg(MsgModbusRsp) m.mb.rsp_handler = m._SolarmanV5__forward_msg m.mb.last_addr = 1 @@ -1607,6 +1544,8 @@ def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusRsp): m.new_data['inverter'] = False m.mb.req_pend = True + m._forward_buffer = bytearray() + m.append_msg(MsgModbusRsp) m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert m.mb.err == 0 @@ -1623,7 +1562,6 @@ def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusRsp): '''Modbus response with a valid Modbus request must be forwarded''' ConfigTsunInv1 m = MemoryStream(MsgModbusRsp) - m.append_msg(MsgModbusRsp) m.mb.rsp_handler = m._SolarmanV5__forward_msg m.mb.last_addr = 1 @@ -1646,11 +1584,13 @@ def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusRsp): assert m.new_data['inverter'] == True m.new_data['inverter'] = False + m._forward_buffer = bytearray() + m.append_msg(MsgModbusRsp) m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert m.mb.err == 5 assert m.msg_count == 2 - assert m._forward_buffer==MsgModbusRsp + assert m._forward_buffer==b'' assert m._send_buffer==b'' # assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} assert m.db.get_db_value(Register.VERSION) == 'V4.0.10' diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index 9555e73..f4fa52e 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -39,6 +39,7 @@ class MemoryStream(Talent): self.addr = 'Test: SrvSide' self.send_msg_ofs = 0 self.test_exception_async_write = False + self.msg_recvd = [] def append_msg(self, msg): self.__msg += msg @@ -63,8 +64,10 @@ class MemoryStream(Talent): return copied_bytes def _timestamp(self): - # return 1700260990000 return 1691246944000 + + def _utc(self): + return 1691239744.0 def createClientStream(self, msg, chunks = (0,)): c = MemoryStream(msg, chunks, False) @@ -73,7 +76,17 @@ class MemoryStream(Talent): return c def _Talent__flush_recv_msg(self) -> None: + self.msg_recvd.append( + { + 'ctrl': int(self.ctrl), + 'msg_id': self.msg_id, + 'header_len': self.header_len, + 'data_len': self.data_len + } + ) + super()._Talent__flush_recv_msg() + self.msg_count += 1 return @@ -173,6 +186,43 @@ def MsgInverterIndTsOffs(): # Data indication from the controller + offset 256 msg += b'\x54\x10T170000000000001\x00\x00\x00\x32\x54\x0a\x54\x53\x4f\x4c\x2d\x4d\x53\x36\x30\x30\x00\x00\x00\x3c\x54\x05\x41\x2c\x42\x2c\x43' return msg +@pytest.fixture +def MsgInverterInd2(): # Data indication from the controller + msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001' + msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08' + msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x02\x00\x00\x01\x2c\x53\x00\x00\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00' + msg += b'\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00' + msg += b'\x00\x00\x00\x01\x98\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00\x00\x01\x9d\x53' + msg += b'\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00' + msg += b'\x01\xf6\x53\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00\x01\xfb\x53\x00\x00\x00' + msg += b'\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00' + msg += b'\x00\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02\x59\x53\x00\x00\x00\x00\x02\x5a' + msg += b'\x53\x00\x00\x00\x00\x02\x5b\x53\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00\x02' + msg += b'\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00' + msg += b'\x02\x66\x53\x00\x00\x00\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02\xbe\x53\x00' + msg += b'\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53' + msg += b'\x00\x00\x00\x00\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00\x00\x00\x00\x02\xca' + msg += b'\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03\x84\x53\x50\x11\x00\x00\x03\xe8\x46\x43\x61\x66\x66\x00' + msg += b'\x00\x04\x4c\x46\x3e\xeb\x85\x1f\x00\x00\x04\xb0\x46\x42\x48\x14\x7b\x00\x00\x05\x14\x53\x00\x17\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05\xdc\x53\x02\x58\x00\x00\x06' + msg += b'\x40\x46\x42\xd3\x66\x66\x00\x00\x06\xa4\x46\x42\x06\x66\x66\x00\x00\x07\x08\x46\x3f\xf4\x7a\xe1\x00\x00\x07\x6c\x46\x42\x81\x00\x00\x00\x00\x07\xd0\x46\x42\x06\x00' + msg += b'\x00\x00\x00\x08\x34\x46\x3f\xae\x14\x7b\x00\x00\x08\x98\x46\x42\x36\xcc\xcd\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60\x46\x00\x00\x00\x00\x00\x00\x09\xc4' + msg += b'\x46\x00\x00\x00\x00\x00\x00\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x3f\xd9\x99\x9a' + msg += b'\x00\x00\x0b\xb8\x46\x41\x8a\xe1\x48\x00\x00\x0c\x1c\x46\x3f\x8a\x3d\x71\x00\x00\x0c\x80\x46\x41\x1b\xd7\x0a\x00\x00\x0c\xe4\x46\x3f\x1e\xb8\x52\x00\x00\x0d\x48\x46' + msg += b'\x40\xf3\xd7\x0a\x00\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00\x00\x00\x00\x00' + msg += b'\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x00\x00\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00' + msg += b'\x00\x00\x11\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53\xff\xff\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\xff\xff\x00\x00\x13\x88\x53\xff' + msg += b'\xff\x00\x00\x13\xec\x53\xff\xff\x00\x00\x14\x50\x53\xff\xff\x00\x00\x14\xb4\x53\xff\xff\x00\x00\x15\x18\x53\xff\xff\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53' + msg += b'\x00\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00\x00\x29\x04\x46\x43\x48\x00\x00' + msg += b'\x00\x00\x29\x68\x46\x42\x48\x33\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42\xce' + msg += b'\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06' + msg += b'\x00\x00\x2d\x50\x53\x00\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0\x53\x00\x03' + msg += b'\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00' + msg += b'\x31\x38\x46\x42\x08\x00\x00\x00\x00\x31\x9c\x53\x00\x05\x00\x00\x32\x00\x53\x04\x00\x00\x00\x32\x64\x53\x00\x01\x00\x00\x32\xc8\x53\x13\x9c\x00\x00\x33\x2c\x53\x0f' + msg += b'\xa0\x00\x00\x33\x90\x53\x00\x4f\x00\x00\x33\xf4\x53\x00\x66\x00\x00\x34\x58\x53\x03\xe8\x00\x00\x34\xbc\x53\x04\x00\x00\x00\x35\x20\x53\x00\x00\x00\x00\x35\x84\x53' + msg += b'\x00\x00\x00\x00\x35\xe8\x53\x00\x00\x00\x00\x36\x4c\x53\x00\x00\x00\x01\x38\x80\x53\x00\x02\x00\x01\x38\x81\x53\x00\x01\x00\x01\x38\x82\x53\x00\x01\x00\x01\x38\x83' + msg += b'\x53\x00\x00' + return msg + @pytest.fixture def MsgInverterIndNew(): # Data indication from DSP V5.0.17 msg = b'\x00\x00\x04\xa0\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001' @@ -392,6 +442,90 @@ def MsgModbusResp21(): msg += b'\x00\x00\x00\x00\x00\x00\x00\xe6\xef' return msg +@pytest.fixture +def BrokenRecvBuf(): # There are two message in the buffer, but the second has overwritten the first partly + msg = b'\x00\x00\x05\x02\x10R170000000000001\x91\x04\x01\x90\x00\x01\x10R170000000000001' + msg += b'\x01\x00\x00\x01\x89\xc6\x63\x61\x08' + msg += b'\x00\x00\x00\xa3\x00\x00\x00\x64\x53\x00\x01' + msg += b'\x00\x00\x00\xc8\x53\x00\x00\x00\x00\x01\x2c\x53\x00\x02\x00\x00' + msg += b'\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53\x00\x00\x00\x00' + msg += b'\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00\x00\x00\x01\x94' + msg += b'\x53\x00\x00\x00\x00\x00\x05\x02\x10\x52\x31\x37\x45\x37\x33\x30' + msg += b'\x37\x30\x32\x31\x44\x30\x30\x36\x41\x91\x04\x01\x90\x00\x01\x10' + msg += b'\x54\x31\x37\x45\x37\x33\x30\x37\x30\x32\x31\x44\x30\x30\x36\x41' + msg += b'\x01\x00\x00\x01\x91\x1c\xe6\x80\xd0\x00\x00\x00\xa3\x00\x00\x00' + msg += b'\x64\x53\x00\x01\x00\x00\x00\xc8\x53\x00\x00\x00\x00\x01\x2c\x53' + msg += b'\x00\x02\x00\x00\x01\x90\x49\x00\x00\x00\x00\x00\x00\x01\x91\x53' + msg += b'\x00\x00\x00\x00\x01\x92\x53\x00\x00\x00\x00\x01\x93\x53\x00\x00' + msg += b'\x00\x00\x01\x94\x53\x00\x00\x00\x00\x01\x95\x53\x00\x00\x00\x00' + msg += b'\x01\x96\x53\x00\x00\x00\x00\x01\x97\x53\x00\x00\x00\x00\x01\x98' + msg += b'\x53\x00\x00\x00\x00\x01\x99\x53\x00\x00\x00\x00\x01\x9a\x53\x00' + msg += b'\x00\x00\x00\x01\x9b\x53\x00\x00\x00\x00\x01\x9c\x53\x00\x00\x00' + msg += b'\x00\x01\x9d\x53\x00\x00\x00\x00\x01\x9e\x53\x00\x00\x00\x00\x01' + msg += b'\x9f\x53\x00\x00\x00\x00\x01\xa0\x53\x00\x00\x00\x00\x01\xf4\x49' + msg += b'\x00\x00\x00\x00\x00\x00\x01\xf5\x53\x00\x00\x00\x00\x01\xf6\x53' + msg += b'\x00\x00\x00\x00\x01\xf7\x53\x00\x00\x00\x00\x01\xf8\x53\x00\x00' + msg += b'\x00\x00\x01\xf9\x53\x00\x00\x00\x00\x01\xfa\x53\x00\x00\x00\x00' + msg += b'\x01\xfb\x53\x00\x00\x00\x00\x01\xfc\x53\x00\x00\x00\x00\x01\xfd' + msg += b'\x53\x00\x00\x00\x00\x01\xfe\x53\x00\x00\x00\x00\x01\xff\x53\x00' + msg += b'\x00\x00\x00\x02\x00\x53\x00\x00\x00\x00\x02\x01\x53\x00\x00\x00' + msg += b'\x00\x02\x02\x53\x00\x00\x00\x00\x02\x03\x53\x00\x00\x00\x00\x02' + msg += b'\x04\x53\x00\x00\x00\x00\x02\x58\x49\x00\x00\x00\x00\x00\x00\x02' + msg += b'\x59\x53\x00\x00\x00\x00\x02\x5a\x53\x00\x00\x00\x00\x02\x5b\x53' + msg += b'\x00\x00\x00\x00\x02\x5c\x53\x00\x00\x00\x00\x02\x5d\x53\x00\x00' + msg += b'\x00\x00\x02\x5e\x53\x00\x00\x00\x00\x02\x5f\x53\x00\x00\x00\x00' + msg += b'\x02\x60\x53\x00\x00\x00\x00\x02\x61\x53\x00\x00\x00\x00\x02\x62' + msg += b'\x53\x00\x00\x00\x00\x02\x63\x53\x00\x00\x00\x00\x02\x64\x53\x00' + msg += b'\x00\x00\x00\x02\x65\x53\x00\x00\x00\x00\x02\x66\x53\x00\x00\x00' + msg += b'\x00\x02\x67\x53\x00\x00\x00\x00\x02\x68\x53\x00\x00\x00\x00\x02' + msg += b'\xbc\x49\x00\x00\x00\x00\x00\x00\x02\xbd\x53\x00\x00\x00\x00\x02' + msg += b'\xbe\x53\x00\x00\x00\x00\x02\xbf\x53\x00\x00\x00\x00\x02\xc0\x53' + msg += b'\x00\x00\x00\x00\x02\xc1\x53\x00\x00\x00\x00\x02\xc2\x53\x00\x00' + msg += b'\x00\x00\x02\xc3\x53\x00\x00\x00\x00\x02\xc4\x53\x00\x00\x00\x00' + msg += b'\x02\xc5\x53\x00\x00\x00\x00\x02\xc6\x53\x00\x00\x00\x00\x02\xc7' + msg += b'\x53\x00\x00\x00\x00\x02\xc8\x53\x00\x00\x00\x00\x02\xc9\x53\x00' + msg += b'\x00\x00\x00\x02\xca\x53\x00\x00\x00\x00\x02\xcb\x53\x00\x00\x00' + msg += b'\x00\x02\xcc\x53\x00\x00\x00\x00\x03\x20\x53\x00\x00\x00\x00\x03' + msg += b'\x84\x53\x51\x09\x00\x00\x03\xe8\x46\x43\x62\xb3\x33\x00\x00\x04' + msg += b'\x4c\x46\x3e\xc2\x8f\x5c\x00\x00\x04\xb0\x46\x42\x48\x00\x00\x00' + msg += b'\x00\x05\x14\x53\x00\x18\x00\x00\x05\x78\x53\x00\x00\x00\x00\x05' + msg += b'\xdc\x53\x02\x58\x00\x00\x06\x40\x46\x42\xae\xcc\xcd\x00\x00\x06' + msg += b'\xa4\x46\x3f\x4c\xcc\xcd\x00\x00\x07\x08\x46\x00\x00\x00\x00\x00' + msg += b'\x00\x07\x6c\x46\x00\x00\x00\x00\x00\x00\x07\xd0\x46\x42\x0a\x66' + msg += b'\x66\x00\x00\x08\x34\x46\x40\x2a\x3d\x71\x00\x00\x08\x98\x46\x42' + msg += b'\xb8\x33\x33\x00\x00\x08\xfc\x46\x00\x00\x00\x00\x00\x00\x09\x60' + msg += b'\x46\x00\x00\x00\x00\x00\x00\x09\xc4\x46\x00\x00\x00\x00\x00\x00' + msg += b'\x0a\x28\x46\x00\x00\x00\x00\x00\x00\x0a\x8c\x46\x00\x00\x00\x00' + msg += b'\x00\x00\x0a\xf0\x46\x00\x00\x00\x00\x00\x00\x0b\x54\x46\x3e\x05' + msg += b'\x1e\xb8\x00\x00\x0b\xb8\x46\x43\xe2\x42\x8f\x00\x00\x0c\x1c\x46' + msg += b'\x00\x00\x00\x00\x00\x00\x0c\x80\x46\x43\x04\x4a\x3d\x00\x00\x0c' + msg += b'\xe4\x46\x3e\x0f\x5c\x29\x00\x00\x0d\x48\x46\x43\xad\x48\xf6\x00' + msg += b'\x00\x0d\xac\x46\x00\x00\x00\x00\x00\x00\x0e\x10\x46\x00\x00\x00' + msg += b'\x00\x00\x00\x0e\x74\x46\x00\x00\x00\x00\x00\x00\x0e\xd8\x46\x00' + msg += b'\x00\x00\x00\x00\x00\x0f\x3c\x53\x00\x00\x00\x00\x0f\xa0\x53\x00' + msg += b'\x00\x00\x00\x10\x04\x53\x55\xaa\x00\x00\x10\x68\x53\x00\x01\x00' + msg += b'\x00\x10\xcc\x53\x00\x00\x00\x00\x11\x30\x53\x00\x00\x00\x00\x11' + msg += b'\x94\x53\x00\x00\x00\x00\x11\xf8\x53\xff\xff\x00\x00\x12\x5c\x53' + msg += b'\x03\x20\x00\x00\x12\xc0\x53\x00\x02\x00\x00\x13\x24\x53\x04\x00' + msg += b'\x00\x00\x13\x88\x53\x04\x00\x00\x00\x13\xec\x53\x04\x00\x00\x00' + msg += b'\x14\x50\x53\x04\x00\x00\x00\x14\xb4\x53\x00\x01\x00\x00\x15\x18' + msg += b'\x53\x08\x04\x00\x00\x15\x7c\x53\x00\x00\x00\x00\x27\x10\x53\x00' + msg += b'\x02\x00\x00\x27\x74\x53\x00\x3c\x00\x00\x27\xd8\x53\x00\x68\x00' + msg += b'\x00\x28\x3c\x53\x05\x00\x00\x00\x28\xa0\x46\x43\x79\x00\x00\x00' + msg += b'\x00\x29\x04\x46\x43\x48\x00\x00\x00\x00\x29\x68\x46\x42\x48\x33' + msg += b'\x33\x00\x00\x29\xcc\x46\x42\x3e\x3d\x71\x00\x00\x2a\x30\x53\x00' + msg += b'\x01\x00\x00\x2a\x94\x46\x43\x37\x00\x00\x00\x00\x2a\xf8\x46\x42' + msg += b'\xce\x00\x00\x00\x00\x2b\x5c\x53\x00\x96\x00\x00\x2b\xc0\x53\x00' + msg += b'\x10\x00\x00\x2c\x24\x46\x43\x90\x00\x00\x00\x00\x2c\x88\x46\x43' + msg += b'\x95\x00\x00\x00\x00\x2c\xec\x53\x00\x06\x00\x00\x2d\x50\x53\x00' + msg += b'\x06\x00\x00\x2d\xb4\x46\x43\x7d\x00\x00\x00\x00\x2e\x18\x46\x42' + msg += b'\x3d\xeb\x85\x00\x00\x2e\x7c\x46\x42\x3d\xeb\x85\x00\x00\x2e\xe0' + msg += b'\x53\x00\x03\x00\x00\x2f\x44\x53\x00\x03\x00\x00\x2f\xa8\x46\x42' + msg += b'\x4d\xeb\x85\x00\x00\x30\x0c\x46\x42\x4d\xeb\x85\x00\x00\x30\x70' + msg += b'\x53\x00\x03\x00\x00\x30\xd4\x53\x00\x03\x00\x00\x31\x38\x46\x42' + msg += b'\x08\x00\x00\x00\x00\x31' + return msg + def test_read_message(MsgContactInfo): m = MemoryStream(MsgContactInfo, (0,)) m.read() # read complete msg, and dispatch msg @@ -412,23 +546,17 @@ def test_read_message_twice(ConfigNoTsunInv1, MsgInverterInd): m.append_msg(MsgInverterInd) m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed - assert m.msg_count == 1 - assert m.id_str == b"R170000000000001" - assert m.unique_id == 'R170000000000001' - assert int(m.ctrl)==145 - assert m.msg_id==4 - assert m.header_len==23 - assert m.data_len==120 - assert m._forward_buffer==b'' - m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert m.msg_count == 2 + assert m.msg_recvd[0]['ctrl']==145 + assert m.msg_recvd[0]['msg_id']==4 + assert m.msg_recvd[0]['header_len']==23 + assert m.msg_recvd[0]['data_len']==120 + assert m.msg_recvd[1]['ctrl']==145 + assert m.msg_recvd[1]['msg_id']==4 + assert m.msg_recvd[1]['header_len']==23 + assert m.msg_recvd[1]['data_len']==120 assert m.id_str == b"R170000000000001" assert m.unique_id == 'R170000000000001' - assert int(m.ctrl)==145 - assert m.msg_id==4 - assert m.header_len==23 - assert m.data_len==120 assert m._forward_buffer==b'' m.close() @@ -499,35 +627,21 @@ def test_read_two_messages(ConfigTsunAllowAll, Msg2ContactInfo,MsgContactResp,Ms m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed - assert m.msg_count == 1 - assert m.id_str == b"R170000000000001" - assert m.unique_id == 'R170000000000001' - assert int(m.ctrl)==145 - assert m.msg_id==0 - assert m.header_len==23 - assert m.data_len==25 - assert m._forward_buffer==b'' - assert m._send_buffer==MsgContactResp - assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 - - m._send_buffer = bytearray(0) # clear send buffer for next test - m.contact_name = b'solarhub' - m.contact_mail = b'solarhub@123456' - m._init_new_client_conn() - assert m._send_buffer==b'\x00\x00\x00,\x10R170000000000001\x91\x00\x08solarhub\x0fsolarhub@123456' - - m._send_buffer = bytearray(0) # clear send buffer for next test - m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert m.msg_count == 2 assert m.id_str == b"R170000000000002" assert m.unique_id == 'R170000000000002' - assert int(m.ctrl)==145 - assert m.msg_id==0 - assert m.header_len==23 - assert m.data_len==25 + m.contact_name = b'solarhub' + m.contact_mail = b'solarhub@123456' + assert m.msg_recvd[0]['ctrl']==145 + assert m.msg_recvd[0]['msg_id']==0 + assert m.msg_recvd[0]['header_len']==23 + assert m.msg_recvd[0]['data_len']==25 + assert m.msg_recvd[1]['ctrl']==145 + assert m.msg_recvd[1]['msg_id']==0 + assert m.msg_recvd[1]['header_len']==23 + assert m.msg_recvd[1]['data_len']==25 assert m._forward_buffer==b'' - assert m._send_buffer==MsgContactResp2 + assert m._send_buffer==MsgContactResp + MsgContactResp2 assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 m._send_buffer = bytearray(0) # clear send buffer for next test @@ -836,10 +950,10 @@ def test_msg_inv_ind(ConfigTsunInv1, MsgInverterInd, MsgInverterIndTsOffs, MsgIn assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 m.close() -def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, MsgInverterAck): +def test_msg_inv_ind1(ConfigTsunInv1, MsgInverterInd2, MsgInverterIndTsOffs, MsgInverterAck): ConfigTsunInv1 tracer.setLevel(logging.DEBUG) - m = MemoryStream(MsgInverterIndNew, (0,)) + m = MemoryStream(MsgInverterInd2, (0,)) m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Invalid_Data_Type'] = 0 m.read() # read complete msg, and dispatch msg @@ -852,11 +966,12 @@ def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, M assert int(m.ctrl)==145 assert m.msg_id==4 assert m.header_len==23 - assert m.data_len==1165 + assert m.data_len==1263 m.ts_offset = 0 m._update_header(m._forward_buffer) - assert m._forward_buffer==MsgInverterIndNew + assert m._forward_buffer==MsgInverterInd2 assert m._send_buffer==MsgInverterAck + assert m.db.get_db_value(Register.TS_GRID) == 1691243349 m.close() def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, MsgInverterAck): @@ -881,6 +996,7 @@ def test_msg_inv_ind2(ConfigTsunInv1, MsgInverterIndNew, MsgInverterIndTsOffs, M assert m._forward_buffer==MsgInverterIndNew assert m._send_buffer==MsgInverterAck assert m.db.get_db_value(Register.INVERTER_STATUS) == None + assert m.db.get_db_value(Register.TS_GRID) == None m.db.db['grid'] = {'Output_Power': 100} m.close() assert m.db.get_db_value(Register.INVERTER_STATUS) == None @@ -1086,6 +1202,23 @@ def test_msg_iterator(): assert test1 == 1 assert test2 == 1 +def test_timestamp_cnv(): + '''test converting inverter timestamps into utc''' + m = MemoryStream(b'') + ts = 1722645998453 # Saturday, 3. August 2024 00:46:38.453 (GMT+2:00) + utc =1722638798.453 # GMT: Friday, 2. August 2024 22:46:38.453 + assert utc == m._utcfromts(ts) + + ts = 1691246944000 # Saturday, 5. August 2023 14:49:04 (GMT+2:00) + utc =1691239744.0 # GMT: Saturday, 5. August 2023 12:49:04 + assert utc == m._utcfromts(ts) + + ts = 1704152544000 # Monday, 1. January 2024 23:42:24 (GMT+1:00) + utc =1704148944.0 # GMT: Monday, 1. January 2024 22:42:24 + assert utc == m._utcfromts(ts) + + m.close() + def test_proxy_counter(): # m = MemoryStream(b'') # m.close() @@ -1286,27 +1419,29 @@ def test_msg_modbus_rsp2(ConfigTsunInv1, MsgModbusResp20): assert m.db.db == {} m.new_data['inverter'] = False + # m.read() # read complete msg, and dispatch msg + # assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + # assert m.mb.err == 0 + # assert m.msg_count == 1 + # assert m._forward_buffer==MsgModbusResp20 + # assert m._send_buffer==b'' + # assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} + # assert m.db.get_db_value(Register.VERSION) == 'V5.1.09' + # assert m.db.get_db_value(Register.TS_GRID) == m._utc() + # assert m.new_data['inverter'] == True + + # m.new_data['inverter'] = False + # m.mb.req_pend = True m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed - assert m.mb.err == 0 - assert m.msg_count == 1 - assert m._forward_buffer==MsgModbusResp20 - assert m._send_buffer==b'' - assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} - assert m.db.get_db_value(Register.VERSION) == 'V5.1.09' - assert m.new_data['inverter'] == True - - m.new_data['inverter'] = False - m.mb.req_pend = True - m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since msg was handled and buffer flushed - assert m.mb.err == 0 + assert m.mb.err == 5 assert m.msg_count == 2 assert m._forward_buffer==MsgModbusResp20 assert m._send_buffer==b'' - assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} + assert m.db.db == {'inverter': {'Version': 'V5.1.09', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} assert m.db.get_db_value(Register.VERSION) == 'V5.1.09' - assert m.new_data['inverter'] == False + assert m.db.get_db_value(Register.TS_GRID) == m._utc() + assert m.new_data['inverter'] == True m.close() @@ -1327,17 +1462,18 @@ def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusResp21): assert m.db.db == {} m.new_data['inverter'] = False - m.read() # read complete msg, and dispatch msg - assert not m.header_valid # must be invalid, since msg was handled and buffer flushed - assert m.mb.err == 0 - assert m.msg_count == 1 - assert m._forward_buffer==MsgModbusResp21 - assert m._send_buffer==b'' - assert m.db.db == {'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} - assert m.db.get_db_value(Register.VERSION) == 'V5.1.0E' - assert m.new_data['inverter'] == True - m.new_data['inverter'] = False - assert m.mb.req_pend == False + # m.read() # read complete msg, and dispatch msg + # assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + # assert m.mb.err == 0 + # assert m.msg_count == 1 + # assert m._forward_buffer==MsgModbusResp21 + # assert m._send_buffer==b'' + # assert m.db.db == {'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} + # assert m.db.get_db_value(Register.VERSION) == 'V5.1.0E' + # assert m.new_data['inverter'] == True + # assert m.db.get_db_value(Register.TS_GRID) == m._utc() + # m.new_data['inverter'] = False + # assert m.mb.req_pend == False m.read() # read complete msg, and dispatch msg assert not m.header_valid # must be invalid, since msg was handled and buffer flushed @@ -1345,9 +1481,10 @@ def test_msg_modbus_rsp3(ConfigTsunInv1, MsgModbusResp21): assert m.msg_count == 2 assert m._forward_buffer==MsgModbusResp21 assert m._send_buffer==b'' - assert m.db.db == {'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} + assert m.db.db == {'inverter': {'Version': 'V5.1.0E', 'Rated_Power': 300}, 'grid': {'Timestamp': m._utc(), 'Voltage': 225.9, 'Current': 0.41, 'Frequency': 49.99, 'Output_Power': 94.8}, 'env': {'Inverter_Temp': 22}, 'input': {'Timestamp': m._utc(), 'pv1': {'Voltage': 0.8, 'Current': 0.0, 'Power': 0.0}, 'pv2': {'Voltage': 34.5, 'Current': 2.89, 'Power': 99.8}, 'pv3': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}, 'pv4': {'Voltage': 0.0, 'Current': 0.0, 'Power': 0.0}}} assert m.db.get_db_value(Register.VERSION) == 'V5.1.0E' - assert m.new_data['inverter'] == False + assert m.db.get_db_value(Register.TS_GRID) == m._utc() + assert m.new_data['inverter'] == True m.close() @@ -1491,6 +1628,28 @@ async def test_modbus_polling(ConfigTsunInv1, MsgGetTime): assert next(m.mb_timer.exp_count) == 4 m.close() +def test_broken_recv_buf(ConfigTsunAllowAll, BrokenRecvBuf): + ConfigTsunAllowAll + m = MemoryStream(BrokenRecvBuf, (0,)) + m.db.stat['proxy']['Unknown_Ctrl'] = 0 + assert m.db.stat['proxy']['Invalid_Data_Type'] == 0 + m.read() # read complete msg, and dispatch msg + assert not m.header_valid # must be invalid, since msg was handled and buffer flushed + assert m.msg_count == 1 + assert m.id_str == b"R170000000000001" + assert m.unique_id == 'R170000000000001' + assert m.msg_recvd[0]['ctrl']==145 + assert m.msg_recvd[0]['msg_id']==4 + assert m.msg_recvd[0]['header_len']==23 + assert m.msg_recvd[0]['data_len']==1263 + # assert m._forward_buffer==b'' + # assert m._send_buffer==b'' + assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 + assert m.db.stat['proxy']['Invalid_Data_Type'] == 1 + + m.close() + + ''' def test_zombie_conn(ConfigTsunInv1, MsgInverterInd): ConfigTsunInv1