SonarCloud setup (#168)

* Code Cleanup (#158)

* print coverage report

* create sonar-project property file

* install all py dependencies in one step

* code cleanup

* reduce cognitive complexity

* do not build on *.yml changes

* optimise versionstring handling (#159)

- Reading the version string from the image updates
  it even if the image is re-pulled without re-deployment

* fix linter warning

* exclude *.pyi filese

* ignore some rules for tests

* cleanup (#160)

* Sonar qube 3 (#163)

fix SonarQube warnings in modbus.py

* Sonar qube 3 (#164)


* fix SonarQube warnings

* Sonar qube 3 (#165)

* cleanup

* Add support for TSUN Titan inverter
Fixes #161


* fix SonarQube warnings

* fix error

* rename field "config"

* SonarQube reads flake8 output

* don't stop on flake8 errors

* flake8 scan only app/src for SonarQube

* update flake8 run

* ignore flake8 C901

* cleanup

* fix linter warnings

* ignore changed *.yml files

* read sensor list solarman data packets

* catch 'No route to' error and log only in debug mode

* fix unit tests

* add sensor_list configuration

* adapt unit tests

* fix SonarQube warnings

* Sonar qube 3 (#166)

* add unittests for mqtt.py

* add mock

* move test requirements into a file

* fix unit tests

* fix formating

* initial version

* fix SonarQube warning
This commit is contained in:
Stefan Allius
2024-08-23 21:24:01 +02:00
committed by GitHub
parent db1169f61f
commit 58b42f7d7c
27 changed files with 899 additions and 500 deletions

View File

@@ -36,9 +36,18 @@ jobs:
timezoneLinux: "Europe/Berlin" timezoneLinux: "Europe/Berlin"
timezoneMacos: "Europe/Berlin" timezoneMacos: "Europe/Berlin"
timezoneWindows: "Europe/Berlin" timezoneWindows: "Europe/Berlin"
# - name: Start Mosquitto
# uses: namoshek/mosquitto-github-action@v1
# with:
# version: '1.6'
# ports: '1883:1883 8883:8883'
# certificates: ${{ github.workspace }}/.ci/tls-certificates
# config: ${{ github.workspace }}/.ci/mosquitto.conf
# password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
# container-name: 'mqtt'
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 # Fetch all history for all tags and branches fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
@@ -46,29 +55,28 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install flake8 pytest pytest-asyncio if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics flake8 --exit-zero --ignore=C901,E121,E123,E126,E133,E226,E241,E242,E704,W503,W504,W505 --format=pylint --output-file=output_flake.txt --exclude=*.pyc app/src/
- name: Test with pytest - name: Test with pytest
run: | run: |
pip install pytest pytest-cov
#pytest app --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
python -m pytest app --cov=app/src --cov-report=xml python -m pytest app --cov=app/src --cov-report=xml
coverage report
- name: Analyze with SonarCloud - name: Analyze with SonarCloud
uses: SonarSource/sonarcloud-github-action@v2.2.0 uses: SonarSource/sonarcloud-github-action@v2.2.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with: with:
projectBaseDir: . projectBaseDir: .
args: args:
-Dsonar.projectKey=s-allius_tsun-gen3-proxy -Dsonar.projectKey=s-allius_tsun-gen3-proxy
-Dsonar.organization=s-allius
-Dsonar.python.version=3.12
-Dsonar.python.coverage.reportPaths=coverage.xml -Dsonar.python.coverage.reportPaths=coverage.xml
-Dsonar.tests=system_tests,app/tests -Dsonar.python.flake8.reportPaths=output_flake.txt
-Dsonar.source=app/src # -Dsonar.docker.hadolint.reportPaths=

View File

@@ -15,5 +15,8 @@
"sonarlint.connectedMode.project": { "sonarlint.connectedMode.project": {
"connectionId": "s-allius", "connectionId": "s-allius",
"projectKey": "s-allius_tsun-gen3-proxy" "projectKey": "s-allius_tsun-gen3-proxy"
},
"files.exclude": {
"**/*.pyi": true
} }
} }

View File

@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased] ## [unreleased]
- Reading the version string from the image updates it even if the image is re-pulled without re-deployment
## [0.10.1] - 2024-08-10 ## [0.10.1] - 2024-08-10
- fix displaying the version string at startup and in HA [#153](https://github.com/s-allius/tsun-gen3-proxy/issues/153) - fix displaying the version string at startup and in HA [#153](https://github.com/s-allius/tsun-gen3-proxy/issues/153)

View File

@@ -34,7 +34,6 @@ ARG GID
ARG LOG_LVL ARG LOG_LVL
ARG environment ARG environment
ENV VERSION=$VERSION
ENV SERVICE_NAME=$SERVICE_NAME ENV SERVICE_NAME=$SERVICE_NAME
ENV UID=$UID ENV UID=$UID
ENV GID=$GID ENV GID=$GID
@@ -63,6 +62,7 @@ RUN python -m pip install --no-cache --no-index /root/wheels/* && \
COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh COPY --chmod=0700 entrypoint.sh /root/entrypoint.sh
COPY config . COPY config .
COPY src . COPY src .
RUN echo ${VERSION} > /proxy-version.txt
RUN date > /build-date.txt RUN date > /build-date.txt
EXPOSE 5005 8127 10000 EXPOSE 5005 8127 10000

View File

@@ -2,6 +2,8 @@
set -e set -e
user="$(id -u)" user="$(id -u)"
export VERSION=$(cat /proxy-version.txt)
echo "######################################################" echo "######################################################"
echo "# prepare: '$SERVICE_NAME' Version:$VERSION" echo "# prepare: '$SERVICE_NAME' Version:$VERSION"
echo "# for running with UserID:$UID, GroupID:$GID" echo "# for running with UserID:$UID, GroupID:$GID"

View File

@@ -0,0 +1,6 @@
flake8
pytest
pytest-asyncio
pytest-cov
mock
coverage

View File

@@ -12,7 +12,7 @@ class Config():
Read config.toml file and sanitize it with read(). Read config.toml file and sanitize it with read().
Get named parts of the config with get()''' Get named parts of the config with get()'''
config = {} act_config = {}
def_config = {} def_config = {}
conf_schema = Schema({ conf_schema = Schema({
'tsun': { 'tsun': {
@@ -51,8 +51,9 @@ class Config():
Optional('monitor_sn', default=0): Use(int), Optional('monitor_sn', default=0): Use(int),
Optional('node_id', default=""): And(Use(str), Optional('node_id', default=""): And(Use(str),
Use(lambda s: s + '/' Use(lambda s: s + '/'
if len(s) > 0 and if len(s) > 0
s[-1] != '/' else s)), and s[-1] != '/'
else s)),
Optional('client_mode'): { Optional('client_mode'): {
'host': Use(str), 'host': Use(str),
Optional('port', default=8899): Optional('port', default=8899):
@@ -60,6 +61,7 @@ class Config():
}, },
Optional('modbus_polling', default=True): Use(bool), Optional('modbus_polling', default=True): Use(bool),
Optional('suggested_area', default=""): Use(str), Optional('suggested_area', default=""): Use(str),
Optional('sensor_list', default=0x2b0): Use(int),
Optional('pv1'): { Optional('pv1'): {
Optional('type'): Use(str), Optional('type'): Use(str),
Optional('manufacturer'): Use(str), Optional('manufacturer'): Use(str),
@@ -84,7 +86,8 @@ class Config():
Optional('type'): Use(str), Optional('type'): Use(str),
Optional('manufacturer'): Use(str), Optional('manufacturer'): Use(str),
} }
}} }
}
}, ignore_extra_keys=True }, ignore_extra_keys=True
) )
@@ -146,17 +149,17 @@ class Config():
config[key] |= usr_config[key] config[key] |= usr_config[key]
try: try:
cls.config = cls.conf_schema.validate(config) cls.act_config = cls.conf_schema.validate(config)
except Exception as error: except Exception as error:
err = f'Config.read: {error}' err = f'Config.read: {error}'
logging.error(err) logging.error(err)
# logging.debug(f'Readed config: "{cls.config}" ') # logging.debug(f'Readed config: "{cls.act_config}" ')
except Exception as error: except Exception as error:
err = f'Config.read: {error}' err = f'Config.read: {error}'
logger.error(err) logger.error(err)
cls.config = {} cls.act_config = {}
return err return err
@@ -166,12 +169,12 @@ class Config():
None it returns the complete config dict''' None it returns the complete config dict'''
if member: if member:
return cls.config.get(member, {}) return cls.act_config.get(member, {})
else: else:
return cls.config return cls.act_config
@classmethod @classmethod
def is_default(cls, member: str) -> bool: def is_default(cls, member: str) -> bool:
'''Check if the member is the default value''' '''Check if the member is the default value'''
return cls.config.get(member) == cls.def_config.get(member) return cls.act_config.get(member) == cls.def_config.get(member)

View File

@@ -139,7 +139,6 @@ class InfosG3(Infos):
i = elms # abort the loop i = elms # abort the loop
elif data_type == 0x41: # 'A' -> Nop ?? elif data_type == 0x41: # 'A' -> Nop ??
# result = struct.unpack_from('!l', buf, ind)[0]
ind += 0 ind += 0
i += 1 i += 1
continue continue
@@ -171,17 +170,17 @@ class InfosG3(Infos):
" not supported") " not supported")
return return
keys, level, unit, must_incr = self._key_obj(info_id) yield from self.__store_result(addr, result, info_id, node_id)
i += 1
def __store_result(self, addr, result, info_id, node_id):
keys, level, unit, must_incr = self._key_obj(info_id)
if keys: if keys:
name, update = self.update_db(keys, must_incr, result) name, update = self.update_db(keys, must_incr, result)
yield keys[0], update yield keys[0], update
else: else:
update = False update = False
name = str(f'info-id.0x{addr:x}') name = str(f'info-id.0x{addr:x}')
if update: if update:
self.tracer.log(level, f'[{node_id}] GEN3: {name} :' self.tracer.log(level, f'[{node_id}] GEN3: {name} :'
f' {result}{unit}') f' {result}{unit}')
i += 1

View File

@@ -1,6 +1,6 @@
import struct import struct
import logging import logging
import pytz from zoneinfo import ZoneInfo
from datetime import datetime from datetime import datetime
from tzlocal import get_localzone from tzlocal import get_localzone
@@ -42,6 +42,7 @@ class Control:
class Talent(Message): class Talent(Message):
MB_START_TIMEOUT = 40 MB_START_TIMEOUT = 40
MB_REGULAR_TIMEOUT = 60 MB_REGULAR_TIMEOUT = 60
TXT_UNKNOWN_CTRL = 'Unknown Ctrl'
def __init__(self, server_side: bool, id_str=b''): def __init__(self, server_side: bool, id_str=b''):
super().__init__(server_side, self.send_modbus_cb, mb_timeout=15) super().__init__(server_side, self.send_modbus_cb, mb_timeout=15)
@@ -75,7 +76,7 @@ class Talent(Message):
self.node_id = 'G3' # will be overwritten in __set_serial_no self.node_id = 'G3' # will be overwritten in __set_serial_no
self.mb_timer = Timer(self.mb_timout_cb, self.node_id) self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
self.mb_timeout = self.MB_REGULAR_TIMEOUT self.mb_timeout = self.MB_REGULAR_TIMEOUT
self.mb_start_timeout = self.MB_START_TIMEOUT self.mb_first_timeout = self.MB_START_TIMEOUT
self.modbus_polling = False self.modbus_polling = False
''' '''
@@ -176,7 +177,7 @@ class Talent(Message):
return return
self.__build_header(0x70, 0x77) self.__build_header(0x70, 0x77)
self._send_buffer += b'\x00\x01\xa3\x28' # fixme self._send_buffer += b'\x00\x01\xa3\x28' # magic ?
self._send_buffer += struct.pack('!B', len(modbus_pdu)) self._send_buffer += struct.pack('!B', len(modbus_pdu))
self._send_buffer += modbus_pdu self._send_buffer += modbus_pdu
self.__finish_send_msg() self.__finish_send_msg()
@@ -246,7 +247,7 @@ class Talent(Message):
def _utcfromts(self, ts: float): def _utcfromts(self, ts: float):
'''converts inverter timestamp into unix time (epoche)''' '''converts inverter timestamp into unix time (epoche)'''
dt = datetime.fromtimestamp(ts/1000, pytz.UTC). \ dt = datetime.fromtimestamp(ts/1000, tz=ZoneInfo("UTC")). \
replace(tzinfo=get_localzone()) replace(tzinfo=get_localzone())
return dt.timestamp() return dt.timestamp()
@@ -354,7 +355,7 @@ class Talent(Message):
else: else:
self.forward() self.forward()
else: else:
logger.warning('Unknown Ctrl') logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl') self.inc_counter('Unknown_Ctrl')
self.forward() self.forward()
@@ -397,7 +398,7 @@ class Talent(Message):
f' offset: {self.ts_offset}') f' offset: {self.ts_offset}')
return # ignore received response return # ignore received response
else: else:
logger.warning('Unknown Ctrl') logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl') self.inc_counter('Unknown_Ctrl')
self.forward() self.forward()
@@ -431,7 +432,7 @@ class Talent(Message):
elif self.ctrl.is_resp(): elif self.ctrl.is_resp():
return # ignore received response return # ignore received response
else: else:
logger.warning('Unknown Ctrl') logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl') self.inc_counter('Unknown_Ctrl')
self.forward() self.forward()
@@ -444,14 +445,14 @@ class Talent(Message):
self.__process_data() self.__process_data()
self.state = State.up # allow MODBUS cmds self.state = State.up # allow MODBUS cmds
if (self.modbus_polling): if (self.modbus_polling):
self.mb_timer.start(self.mb_start_timeout) self.mb_timer.start(self.mb_first_timeout)
self.db.set_db_def_value(Register.POLLING_INTERVAL, self.db.set_db_def_value(Register.POLLING_INTERVAL,
self.mb_timeout) self.mb_timeout)
elif self.ctrl.is_resp(): elif self.ctrl.is_resp():
return # ignore received response return # ignore received response
else: else:
logger.warning('Unknown Ctrl') logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl') self.inc_counter('Unknown_Ctrl')
self.forward() self.forward()
@@ -471,7 +472,7 @@ class Talent(Message):
elif self.ctrl.is_ind(): elif self.ctrl.is_ind():
pass # Ok, nothing to do pass # Ok, nothing to do
else: else:
logger.warning('Unknown Ctrl') logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl') self.inc_counter('Unknown_Ctrl')
self.forward() self.forward()
@@ -519,7 +520,7 @@ class Talent(Message):
self.new_data[key] = True self.new_data[key] = True
self.modbus_elms += 1 # count for unit tests self.modbus_elms += 1 # count for unit tests
else: else:
logger.warning('Unknown Ctrl') logger.warning(self.TXT_UNKNOWN_CTRL)
self.inc_counter('Unknown_Ctrl') self.inc_counter('Unknown_Ctrl')
self.forward() self.forward()

View File

@@ -20,9 +20,11 @@ class RegisterMap:
0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501 0x4102001c: {'reg': Register.SIGNAL_STRENGTH, 'fmt': '<B', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501
0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501 0x4102001e: {'reg': Register.CHIP_MODEL, 'fmt': '!40s'}, # noqa: E501
0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501 0x4102004c: {'reg': Register.IP_ADDRESS, 'fmt': '!16s'}, # noqa: E501
0x4102005f: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501 0x41020064: {'reg': Register.COLLECTOR_FW_VERSION, 'fmt': '!40s'}, # noqa: E501
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501 0x4201000c: {'reg': Register.SENSOR_LIST, 'fmt': '<H', 'eval': "f'{result:04x}'"}, # noqa: E501
0x4201001c: {'reg': Register.POWER_ON_TIME, 'fmt': '<H', 'ratio': 1, 'dep': ProxyMode.SERVER}, # noqa: E501, or packet number
0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501 0x42010020: {'reg': Register.SERIAL_NUMBER, 'fmt': '!16s'}, # noqa: E501
0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501 0x420100c0: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501 0x420100d0: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
@@ -118,15 +120,7 @@ class InfosG3P(Infos):
if not isinstance(row, dict): if not isinstance(row, dict):
continue continue
info_id = row['reg'] info_id = row['reg']
fmt = row['fmt'] result = self.__get_value(buf, addr, row)
res = struct.unpack_from(fmt, buf, addr)
result = res[0]
if isinstance(result, (bytearray, bytes)):
result = result.decode().split('\x00')[0]
if 'eval' in row:
result = eval(row['eval'])
if 'ratio' in row:
result = round(result * row['ratio'], 2)
keys, level, unit, must_incr = self._key_obj(info_id) keys, level, unit, must_incr = self._key_obj(info_id)
@@ -140,3 +134,16 @@ class InfosG3P(Infos):
if update: if update:
self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}' self.tracer.log(level, f'[{node_id}] GEN3PLUS: {name}'
f' : {result}{unit}') f' : {result}{unit}')
def __get_value(self, buf, idx, row):
'''Get a value from buf and interpret as in row'''
fmt = row['fmt']
res = struct.unpack_from(fmt, buf, idx)
result = res[0]
if isinstance(result, (bytearray, bytes)):
result = result.decode().split('\x00')[0]
if 'eval' in row:
result = eval(row['eval'])
if 'ratio' in row:
result = round(result * row['ratio'], 2)
return result

View File

@@ -57,9 +57,11 @@ class SolarmanV5(Message):
'''regular Modbus polling time in server mode''' '''regular Modbus polling time in server mode'''
MB_CLIENT_DATA_UP = 30 MB_CLIENT_DATA_UP = 30
'''Data up time in client mode''' '''Data up time in client mode'''
HDR_FMT = '<BLLL'
'''format string for packing of the header'''
def __init__(self, server_side: bool, client_mode: bool): def __init__(self, server_side: bool, client_mode: bool):
super().__init__(server_side, self.send_modbus_cb, mb_timeout=5) super().__init__(server_side, self.send_modbus_cb, mb_timeout=8)
self.header_len = 11 # overwrite construcor in class Message self.header_len = 11 # overwrite construcor in class Message
self.control = 0 self.control = 0
@@ -133,9 +135,10 @@ class SolarmanV5(Message):
self.node_id = 'G3P' # will be overwritten in __set_serial_no self.node_id = 'G3P' # will be overwritten in __set_serial_no
self.mb_timer = Timer(self.mb_timout_cb, self.node_id) self.mb_timer = Timer(self.mb_timout_cb, self.node_id)
self.mb_timeout = self.MB_REGULAR_TIMEOUT self.mb_timeout = self.MB_REGULAR_TIMEOUT
self.mb_start_timeout = self.MB_START_TIMEOUT self.mb_first_timeout = self.MB_START_TIMEOUT
'''timer value for next Modbus polling request''' '''timer value for next Modbus polling request'''
self.modbus_polling = False self.modbus_polling = False
self.sensor_list = 0x0000
''' '''
Our puplic methods Our puplic methods
@@ -180,16 +183,23 @@ class SolarmanV5(Message):
if self.state is not State.up: if self.state is not State.up:
self.state = State.up self.state = State.up
if (self.modbus_polling): if (self.modbus_polling):
self.mb_timer.start(self.mb_start_timeout) self.mb_timer.start(self.mb_first_timeout)
self.db.set_db_def_value(Register.POLLING_INTERVAL, self.db.set_db_def_value(Register.POLLING_INTERVAL,
self.mb_timeout) self.mb_timeout)
def __set_config_parms(self, inv: dict):
'''init connection with params from the configuration'''
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']
self.sensor_list = inv['sensor_list']
def __set_serial_no(self, snr: int): def __set_serial_no(self, snr: int):
'''check the serial number and configure the inverter connection'''
serial_no = str(snr) serial_no = str(snr)
if self.unique_id == serial_no: if self.unique_id == serial_no:
logger.debug(f'SerialNo: {serial_no}') logger.debug(f'SerialNo: {serial_no}')
else: else:
found = False
inverters = Config.get('inverters') inverters = Config.get('inverters')
# logger.debug(f'Inverters: {inverters}') # logger.debug(f'Inverters: {inverters}')
@@ -197,14 +207,11 @@ class SolarmanV5(Message):
# logger.debug(f'key: {key} -> {inv}') # logger.debug(f'key: {key} -> {inv}')
if (type(inv) is dict and 'monitor_sn' in inv if (type(inv) is dict and 'monitor_sn' in inv
and inv['monitor_sn'] == snr): and inv['monitor_sn'] == snr):
found = True self.__set_config_parms(inv)
self.node_id = inv['node_id']
self.sug_area = inv['suggested_area']
self.modbus_polling = inv['modbus_polling']
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
self.db.set_pv_module_details(inv) self.db.set_pv_module_details(inv)
logger.debug(f'SerialNo {serial_no} allowed! area:{self.sug_area}') # noqa: E501
if not found: break
else:
self.node_id = '' self.node_id = ''
self.sug_area = '' self.sug_area = ''
if 'allow_all' not in inverters or not inverters['allow_all']: if 'allow_all' not in inverters or not inverters['allow_all']:
@@ -212,7 +219,7 @@ class SolarmanV5(Message):
self.unique_id = None self.unique_id = None
logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501 logger.warning(f'ignore message from unknow inverter! (SerialNo: {serial_no})') # noqa: E501
return return
logger.debug(f'SerialNo {serial_no} not known but accepted!') logger.warning(f'SerialNo {serial_no} not known but accepted!')
self.unique_id = serial_no self.unique_id = serial_no
@@ -225,6 +232,12 @@ class SolarmanV5(Message):
if self.header_valid and len(self._recv_buffer) >= \ if self.header_valid and len(self._recv_buffer) >= \
(self.header_len + self.data_len+2): (self.header_len + self.data_len+2):
self.__process_complete_received_msg()
self.__flush_recv_msg()
else:
return 0 # wait 0s before sending a response
def __process_complete_received_msg(self):
log_lvl = self.log_lvl.get(self.control, logging.WARNING) log_lvl = self.log_lvl.get(self.control, logging.WARNING)
if callable(log_lvl): if callable(log_lvl):
log_lvl = log_lvl() log_lvl = log_lvl()
@@ -235,12 +248,8 @@ class SolarmanV5(Message):
+ self.data_len + 2): + self.data_len + 2):
if self.state == State.init: if self.state == State.init:
self.state = State.received self.state = State.received
self.__set_serial_no(self.snr) self.__set_serial_no(self.snr)
self.__dispatch_msg() self.__dispatch_msg()
self.__flush_recv_msg()
else:
return 0 # wait 0s before sending a response
def forward(self, buffer, buflen) -> None: def forward(self, buffer, buflen) -> None:
'''add the actual receive msg to the forwarding queue''' '''add the actual receive msg to the forwarding queue'''
@@ -401,7 +410,7 @@ class SolarmanV5(Message):
return return
self.__build_header(0x4510) self.__build_header(0x4510)
self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD, self._send_buffer += struct.pack('<BHLLL', self.MB_RTU_CMD,
0x2b0, 0, 0, 0) self.sensor_list, 0, 0, 0)
self._send_buffer += pdu self._send_buffer += pdu
self.__finish_send_msg() self.__finish_send_msg()
hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:', hex_dump_memory(log_lvl, f'Send Modbus {state}:{self.addr}:',
@@ -450,8 +459,8 @@ class SolarmanV5(Message):
self.forward_at_cmd_resp = False self.forward_at_cmd_resp = False
self.__build_header(0x4510) self.__build_header(0x4510)
self._send_buffer += struct.pack(f'<BHLLL{len(at_cmd)}sc', self.AT_CMD, self._send_buffer += struct.pack(f'<BHLLL{len(at_cmd)}sc', self.AT_CMD,
2, 0, 0, 0, at_cmd.encode('utf-8'), 0x0002, 0, 0, 0,
b'\r') at_cmd.encode('utf-8'), b'\r')
self.__finish_send_msg() self.__finish_send_msg()
try: try:
await self.async_write('Send AT Command:') await self.async_write('Send AT Command:')
@@ -500,7 +509,7 @@ class SolarmanV5(Message):
def msg_dev_ind(self): def msg_dev_ind(self):
data = self._recv_buffer[self.header_len:] data = self._recv_buffer[self.header_len:]
result = struct.unpack_from('<BLLL', data, 0) result = struct.unpack_from(self.HDR_FMT, data, 0)
ftype = result[0] # always 2 ftype = result[0] # always 2
total = result[1] total = result[1]
tim = result[2] tim = result[2]
@@ -514,6 +523,8 @@ class SolarmanV5(Message):
else: else:
ts = None ts = None
self.__process_data(ftype, ts) self.__process_data(ftype, ts)
self.sensor_list = int(self.db.get_db_value(Register.SENSOR_LIST, 0),
16)
self.__forward_msg() self.__forward_msg()
self.__send_ack_rsp(0x1110, ftype) self.__send_ack_rsp(0x1110, ftype)
@@ -521,12 +532,16 @@ class SolarmanV5(Message):
data = self._recv_buffer data = self._recv_buffer
result = struct.unpack_from('<BHLLLHL', data, self.header_len) result = struct.unpack_from('<BHLLLHL', data, self.header_len)
ftype = result[0] # 1 or 0x81 ftype = result[0] # 1 or 0x81
sensor = result[1]
total = result[2] total = result[2]
tim = result[3] tim = result[3]
if 1 == ftype: if 1 == ftype:
self.time_ofs = result[4] self.time_ofs = result[4]
unkn = result[5] unkn = result[5]
cnt = result[6] cnt = result[6]
if sensor != self.sensor_list:
logger.warning(f'Unexpected Sensor-List:{sensor:04x}'
f' (!={self.sensor_list:04x})')
logger.info(f'ftype:{ftype:02x} timer:{tim:08x}s' logger.info(f'ftype:{ftype:02x} timer:{tim:08x}s'
f' ??: {unkn:04x} cnt:{cnt}') f' ??: {unkn:04x} cnt:{cnt}')
if self.time_ofs: if self.time_ofs:
@@ -543,7 +558,7 @@ class SolarmanV5(Message):
def msg_sync_start(self): def msg_sync_start(self):
data = self._recv_buffer[self.header_len:] data = self._recv_buffer[self.header_len:]
result = struct.unpack_from('<BLLL', data, 0) result = struct.unpack_from(self.HDR_FMT, data, 0)
ftype = result[0] ftype = result[0]
total = result[1] total = result[1]
self.time_ofs = result[3] self.time_ofs = result[3]
@@ -608,6 +623,12 @@ class SolarmanV5(Message):
self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501 self.publish_mqtt(f'{self.entity_prfx}{node_id}{key}', data_json) # noqa: E501
return return
elif ftype == self.MB_RTU_CMD: elif ftype == self.MB_RTU_CMD:
self.__modbus_command_rsp(data)
return
self.__forward_msg()
def __modbus_command_rsp(self, data):
'''precess MODBUS RTU response'''
valid = data[1] valid = data[1]
modbus_msg_len = self.data_len - 14 modbus_msg_len = self.data_len - 14
# logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}') # logger.debug(f'modbus_len:{modbus_msg_len} accepted:{valid}')
@@ -615,7 +636,6 @@ class SolarmanV5(Message):
# logger.info(f'first byte modbus:{data[14]}') # logger.info(f'first byte modbus:{data[14]}')
inv_update = False inv_update = False
self.modbus_elms = 0 self.modbus_elms = 0
for key, update, _ in self.mb.recv_resp(self.db, data[14:], for key, update, _ in self.mb.recv_resp(self.db, data[14:],
self.node_id): self.node_id):
self.modbus_elms += 1 self.modbus_elms += 1
@@ -624,11 +644,8 @@ class SolarmanV5(Message):
inv_update = True inv_update = True
self._set_mqtt_timestamp(key, self._timestamp()) self._set_mqtt_timestamp(key, self._timestamp())
self.new_data[key] = True self.new_data[key] = True
if inv_update: if inv_update:
self.__build_model_name() self.__build_model_name()
return
self.__forward_msg()
def msg_hbeat_ind(self): def msg_hbeat_ind(self):
data = self._recv_buffer[self.header_len:] data = self._recv_buffer[self.header_len:]
@@ -641,7 +658,7 @@ class SolarmanV5(Message):
def msg_sync_end(self): def msg_sync_end(self):
data = self._recv_buffer[self.header_len:] data = self._recv_buffer[self.header_len:]
result = struct.unpack_from('<BLLL', data, 0) result = struct.unpack_from(self.HDR_FMT, data, 0)
ftype = result[0] ftype = result[0]
total = result[1] total = result[1]
self.time_ofs = result[3] self.time_ofs = result[3]

View File

@@ -96,6 +96,7 @@ class Register(Enum):
HEARTBEAT_INTERVAL = 406 HEARTBEAT_INTERVAL = 406
IP_ADDRESS = 407 IP_ADDRESS = 407
POLLING_INTERVAL = 408 POLLING_INTERVAL = 408
SENSOR_LIST = 409
EVENT_401 = 500 EVENT_401 = 500
EVENT_402 = 501 EVENT_402 = 501
EVENT_403 = 502 EVENT_403 = 502
@@ -149,10 +150,21 @@ class ClrAtMidnight:
class Infos: class Infos:
LIGHTNING = 'mdi:lightning-bolt'
COUNTER = 'mdi:counter'
GAUGE = 'mdi:gauge'
SOLAR_POWER_VAR = 'mdi:solar-power-variant'
SOLAR_POWER = 'mdi:solar-power'
WIFI = 'mdi:wifi'
UPDATE = 'mdi:update'
DAILY_GEN = 'Daily Generation'
TOTAL_GEN = 'Total Generation'
FMT_INT = '| int'
FMT_FLOAT = '| float'
FMT_STRING_SEC = '| string + " s"'
stat = {} stat = {}
app_name = os.getenv('SERVICE_NAME', 'proxy') app_name = os.getenv('SERVICE_NAME', 'proxy')
version = os.getenv('VERSION', 'unknown') version = os.getenv('VERSION', 'unknown')
new_stat_data = {} new_stat_data = {}
@classmethod @classmethod
@@ -218,9 +230,9 @@ class Infos:
Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.SERIAL_NUMBER: {'name': ['inverter', 'Serial_Number'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.EQUIPMENT_MODEL: {'name': ['inverter', 'Equipment_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.NO_INPUTS: {'name': ['inverter', 'No_Inputs'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.MAX_DESIGNED_POWER: {'name': ['inverter', 'Max_Designed_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'designed_power_', 'val_tpl': __designed_power_val_tpl, 'name': 'Max Designed Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.RATED_POWER: {'name': ['inverter', 'Rated_Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'rated_power_', 'val_tpl': __rated_power_val_tpl, 'name': 'Rated Power', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': 'mdi:lightning-bolt', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.OUTPUT_COEFFICIENT: {'name': ['inverter', 'Output_Coefficient'], 'level': logging.INFO, 'unit': '%', 'ha': {'dev': 'inverter', 'dev_cla': None, 'stat_cla': None, 'id': 'output_coef_', 'val_tpl': __output_coef_val_tpl, 'name': 'Output Coefficient', 'icon': LIGHTNING, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.PV1_MANUFACTURER: {'name': ['inverter', 'PV1_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.PV1_MODEL: {'name': ['inverter', 'PV1_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
Register.PV2_MANUFACTURER: {'name': ['inverter', 'PV2_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.PV2_MANUFACTURER: {'name': ['inverter', 'PV2_Manufacturer'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -235,19 +247,19 @@ class Infos:
Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.PV6_MODEL: {'name': ['inverter', 'PV6_Model'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
# proxy: # proxy:
Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': '| int', 'name': 'Active Inverter Connections', 'icon': 'mdi:counter'}}, # noqa: E501 Register.INVERTER_CNT: {'name': ['proxy', 'Inverter_Cnt'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_count_', 'fmt': FMT_INT, 'name': 'Active Inverter Connections', 'icon': COUNTER}}, # noqa: E501
Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': '| int', 'name': 'Unknown Serial No', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.UNKNOWN_SNR: {'name': ['proxy', 'Unknown_SNR'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_snr_', 'fmt': FMT_INT, 'name': 'Unknown Serial No', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': '| int', 'name': 'Unknown Msg Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.UNKNOWN_MSG: {'name': ['proxy', 'Unknown_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_msg_', 'fmt': FMT_INT, 'name': 'Unknown Msg Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': '| int', 'name': 'Invalid Data Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.INVALID_DATA_TYPE: {'name': ['proxy', 'Invalid_Data_Type'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_data_type_', 'fmt': FMT_INT, 'name': 'Invalid Data Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': '| int', 'name': 'Internal Error', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501 Register.INTERNAL_ERROR: {'name': ['proxy', 'Internal_Error'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'intern_err_', 'fmt': FMT_INT, 'name': 'Internal Error', 'icon': COUNTER, 'ent_cat': 'diagnostic', 'en': False}}, # noqa: E501
Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': '| int', 'name': 'Unknown Control Type', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.UNKNOWN_CTRL: {'name': ['proxy', 'Unknown_Ctrl'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'unknown_ctrl_', 'fmt': FMT_INT, 'name': 'Unknown Control Type', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': '| int', 'name': 'OTA Start Cmd', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.OTA_START_MSG: {'name': ['proxy', 'OTA_Start_Msg'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'ota_start_cmd_', 'fmt': FMT_INT, 'name': 'OTA Start Cmd', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': '| int', 'name': 'Internal SW Exception', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.SW_EXCEPTION: {'name': ['proxy', 'SW_Exception'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'sw_exception_', 'fmt': FMT_INT, 'name': 'Internal SW Exception', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': '| int', 'name': 'Invalid Message Format', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.INVALID_MSG_FMT: {'name': ['proxy', 'Invalid_Msg_Format'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_msg_fmt_', 'fmt': FMT_INT, 'name': 'Invalid Message Format', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': '| int', 'name': 'AT Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.AT_COMMAND: {'name': ['proxy', 'AT_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_', 'fmt': FMT_INT, 'name': 'AT Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': '| int', 'name': 'AT Command Blocked', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.AT_COMMAND_BLOCKED: {'name': ['proxy', 'AT_Command_Blocked'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'at_cmd_blocked_', 'fmt': FMT_INT, 'name': 'AT Command Blocked', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': '| int', 'name': 'Modbus Command', 'icon': 'mdi:counter', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.MODBUS_COMMAND: {'name': ['proxy', 'Modbus_Command'], 'singleton': True, 'ha': {'dev': 'proxy', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'modbus_cmd_', 'fmt': FMT_INT, 'name': 'Modbus Command', 'icon': COUNTER, 'ent_cat': 'diagnostic'}}, # noqa: E501
# 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':'| float','name': 'Grid Voltage'}}, # noqa: E501 # 0xffffff03: {'name':['proxy', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha':{'dev':'proxy', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id':'proxy_volt_', 'fmt':FMT_FLOAT,'name': 'Grid Voltage'}}, # noqa: E501
# events # events
Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501 Register.EVENT_401: {'name': ['events', '401_'], 'level': logging.DEBUG, 'unit': ''}, # noqa: E501
@@ -269,60 +281,61 @@ class Infos:
# grid measures: # grid measures:
Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 Register.TS_GRID: {'name': ['grid', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
Register.GRID_VOLTAGE: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': '| float', 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.GRID_VOLTAGE: {'name': ['grid', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'inverter', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'out_volt_', 'fmt': FMT_FLOAT, 'name': 'Grid Voltage', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.GRID_CURRENT: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': '| float', 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.GRID_CURRENT: {'name': ['grid', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'inverter', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'out_cur_', 'fmt': FMT_FLOAT, 'name': 'Grid Current', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': '| float', 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.GRID_FREQUENCY: {'name': ['grid', 'Frequency'], 'level': logging.DEBUG, 'unit': 'Hz', 'ha': {'dev': 'inverter', 'dev_cla': 'frequency', 'stat_cla': 'measurement', 'id': 'out_freq_', 'fmt': FMT_FLOAT, 'name': 'Grid Frequency', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': '| float', 'name': 'Power'}}, # noqa: E501 Register.OUTPUT_POWER: {'name': ['grid', 'Output_Power'], 'level': logging.INFO, 'unit': 'W', 'ha': {'dev': 'inverter', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'out_power_', 'fmt': FMT_FLOAT, 'name': 'Power'}}, # noqa: E501
Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': '| int', 'name': 'Temperature'}}, # noqa: E501 Register.INVERTER_TEMP: {'name': ['env', 'Inverter_Temp'], 'level': logging.DEBUG, 'unit': '°C', 'ha': {'dev': 'inverter', 'dev_cla': 'temperature', 'stat_cla': 'measurement', 'id': 'temp_', 'fmt': FMT_INT, 'name': 'Temperature'}}, # noqa: E501
Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501 Register.INVERTER_STATUS: {'name': ['env', 'Inverter_Status'], 'level': logging.INFO, 'unit': '', 'ha': {'dev': 'inverter', 'comp': 'sensor', 'dev_cla': None, 'stat_cla': None, 'id': 'inv_status_', 'name': 'Inverter Status', 'val_tpl': __status_type_val_tpl, 'icon': 'mdi:power'}}, # noqa: E501
# input measures: # input measures:
Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 Register.TS_INPUT: {'name': ['input', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV1_VOLTAGE: {'name': ['input', 'pv1', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv1', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv1_', 'val_tpl': "{{ (value_json['pv1']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV1_CURRENT: {'name': ['input', 'pv1', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv1', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv1_', 'val_tpl': "{{ (value_json['pv1']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501 Register.PV1_POWER: {'name': ['input', 'pv1', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv1', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv1_', 'val_tpl': "{{ (value_json['pv1']['Power'] | float)}}"}}, # noqa: E501
Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV2_VOLTAGE: {'name': ['input', 'pv2', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv2', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv2_', 'val_tpl': "{{ (value_json['pv2']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV2_CURRENT: {'name': ['input', 'pv2', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv2', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv2_', 'val_tpl': "{{ (value_json['pv2']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501 Register.PV2_POWER: {'name': ['input', 'pv2', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv2', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv2_', 'val_tpl': "{{ (value_json['pv2']['Power'] | float)}}"}}, # noqa: E501
Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV3_VOLTAGE: {'name': ['input', 'pv3', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv3', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv3_', 'val_tpl': "{{ (value_json['pv3']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV3_CURRENT: {'name': ['input', 'pv3', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv3', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv3_', 'val_tpl': "{{ (value_json['pv3']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501 Register.PV3_POWER: {'name': ['input', 'pv3', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv3', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv3_', 'val_tpl': "{{ (value_json['pv3']['Power'] | float)}}"}}, # noqa: E501
Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV4_VOLTAGE: {'name': ['input', 'pv4', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv4', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv4_', 'val_tpl': "{{ (value_json['pv4']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV4_CURRENT: {'name': ['input', 'pv4', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv4', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv4_', 'val_tpl': "{{ (value_json['pv4']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501 Register.PV4_POWER: {'name': ['input', 'pv4', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv4', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv4_', 'val_tpl': "{{ (value_json['pv4']['Power'] | float)}}"}}, # noqa: E501
Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV5_VOLTAGE: {'name': ['input', 'pv5', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv5', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv5_', 'val_tpl': "{{ (value_json['pv5']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV5_CURRENT: {'name': ['input', 'pv5', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv5', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv5_', 'val_tpl': "{{ (value_json['pv5']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV5_POWER: {'name': ['input', 'pv5', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv5', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv5_', 'val_tpl': "{{ (value_json['pv5']['Power'] | float)}}"}}, # noqa: E501 Register.PV5_POWER: {'name': ['input', 'pv5', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv5', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv5_', 'val_tpl': "{{ (value_json['pv5']['Power'] | float)}}"}}, # noqa: E501
Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV6_VOLTAGE: {'name': ['input', 'pv6', 'Voltage'], 'level': logging.DEBUG, 'unit': 'V', 'ha': {'dev': 'input_pv6', 'dev_cla': 'voltage', 'stat_cla': 'measurement', 'id': 'volt_pv6_', 'val_tpl': "{{ (value_json['pv6']['Voltage'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': 'mdi:gauge', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.PV6_CURRENT: {'name': ['input', 'pv6', 'Current'], 'level': logging.DEBUG, 'unit': 'A', 'ha': {'dev': 'input_pv6', 'dev_cla': 'current', 'stat_cla': 'measurement', 'id': 'cur_pv6_', 'val_tpl': "{{ (value_json['pv6']['Current'] | float)}}", 'icon': GAUGE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.PV6_POWER: {'name': ['input', 'pv6', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv6', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv6_', 'val_tpl': "{{ (value_json['pv6']['Power'] | float)}}"}}, # noqa: E501 Register.PV6_POWER: {'name': ['input', 'pv6', 'Power'], 'level': logging.DEBUG, 'unit': 'W', 'ha': {'dev': 'input_pv6', 'dev_cla': 'power', 'stat_cla': 'measurement', 'id': 'power_pv6_', 'val_tpl': "{{ (value_json['pv6']['Power'] | float)}}"}}, # noqa: E501
Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.PV1_DAILY_GENERATION: {'name': ['input', 'pv1', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv1_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv1']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.PV1_TOTAL_GENERATION: {'name': ['input', 'pv1', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv1', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv1_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv1']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.PV2_DAILY_GENERATION: {'name': ['input', 'pv2', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv2_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv2']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
Register.PV2_TOTAL_GENERATION: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.PV2_TOTAL_GENERATION: {'name': ['input', 'pv2', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv2', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv2_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv2']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
Register.PV3_DAILY_GENERATION: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.PV3_DAILY_GENERATION: {'name': ['input', 'pv3', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv3_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv3']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
Register.PV3_TOTAL_GENERATION: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.PV3_TOTAL_GENERATION: {'name': ['input', 'pv3', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv3', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv3_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv3']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
Register.PV4_DAILY_GENERATION: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.PV4_DAILY_GENERATION: {'name': ['input', 'pv4', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv4_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv4']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
Register.PV4_TOTAL_GENERATION: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.PV4_TOTAL_GENERATION: {'name': ['input', 'pv4', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv4', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv4_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv4']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
Register.PV5_DAILY_GENERATION: {'name': ['input', 'pv5', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv5_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv5']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.PV5_DAILY_GENERATION: {'name': ['input', 'pv5', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv5_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv5']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
Register.PV5_TOTAL_GENERATION: {'name': ['input', 'pv5', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv5_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv5']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.PV5_TOTAL_GENERATION: {'name': ['input', 'pv5', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv5', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv5_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv5']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
Register.PV6_DAILY_GENERATION: {'name': ['input', 'pv6', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv6_', 'name': 'Daily Generation', 'val_tpl': "{{ (value_json['pv6']['Daily_Generation'] | float)}}", 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.PV6_DAILY_GENERATION: {'name': ['input', 'pv6', 'Daily_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_pv6_', 'name': DAILY_GEN, 'val_tpl': "{{ (value_json['pv6']['Daily_Generation'] | float)}}", 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
Register.PV6_TOTAL_GENERATION: {'name': ['input', 'pv6', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv6_', 'name': 'Total Generation', 'val_tpl': "{{ (value_json['pv6']['Total_Generation'] | float)}}", 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.PV6_TOTAL_GENERATION: {'name': ['input', 'pv6', 'Total_Generation'], 'level': logging.DEBUG, 'unit': 'kWh', 'ha': {'dev': 'input_pv6', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_pv6_', 'name': TOTAL_GEN, 'val_tpl': "{{ (value_json['pv6']['Total_Generation'] | float)}}", 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
# total: # total:
Register.TS_TOTAL: {'name': ['total', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501 Register.TS_TOTAL: {'name': ['total', 'Timestamp'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
Register.DAILY_GENERATION: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': '| float', 'name': 'Daily Generation', 'icon': 'mdi:solar-power-variant', 'must_incr': True}}, # noqa: E501 Register.DAILY_GENERATION: {'name': ['total', 'Daily_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total_increasing', 'id': 'daily_gen_', 'fmt': FMT_FLOAT, 'name': DAILY_GEN, 'icon': SOLAR_POWER_VAR, 'must_incr': True}}, # noqa: E501
Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': '| float', 'name': 'Total Generation', 'icon': 'mdi:solar-power', 'must_incr': True}}, # noqa: E501 Register.TOTAL_GENERATION: {'name': ['total', 'Total_Generation'], 'level': logging.INFO, 'unit': 'kWh', 'ha': {'dev': 'inverter', 'dev_cla': 'energy', 'stat_cla': 'total', 'id': 'total_gen_', 'fmt': FMT_FLOAT, 'name': TOTAL_GEN, 'icon': SOLAR_POWER, 'must_incr': True}}, # noqa: E501
# controller: # controller:
Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': '| int', 'name': 'Signal Strength', 'icon': 'mdi:wifi'}}, # noqa: E501 Register.SIGNAL_STRENGTH: {'name': ['controller', 'Signal_Strength'], 'level': logging.DEBUG, 'unit': '%', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': 'measurement', 'id': 'signal_', 'fmt': FMT_INT, 'name': 'Signal Strength', 'icon': WIFI}}, # noqa: E501
Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': '| int', 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.POWER_ON_TIME: {'name': ['controller', 'Power_On_Time'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': 'duration', 'stat_cla': 'measurement', 'id': 'power_on_time_', 'fmt': FMT_INT, 'name': 'Power on Time', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.COLLECT_INTERVAL: {'name': ['controller', 'Collect_Interval'], 'level': logging.DEBUG, 'unit': 'min', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_collect_intval_', 'fmt': '| string + " min"', 'name': 'Data Collect Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': '| int', 'name': 'Connect Count', 'icon': 'mdi:counter', 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.CONNECT_COUNT: {'name': ['controller', 'Connect_Count'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'connect_count_', 'fmt': FMT_INT, 'name': 'Connect Count', 'icon': COUNTER, 'comp': 'sensor', 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': 'mdi:wifi'}}, # noqa: E501 Register.COMMUNICATION_TYPE: {'name': ['controller', 'Communication_Type'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'comm_type_', 'name': 'Communication Type', 'val_tpl': __comm_type_val_tpl, 'comp': 'sensor', 'icon': WIFI}}, # noqa: E501
Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': '| string + " s"', 'name': 'Data Up Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.DATA_UP_INTERVAL: {'name': ['controller', 'Data_Up_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'data_up_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Data Up Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': '| string + " s"', 'name': 'Heartbeat Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.HEARTBEAT_INTERVAL: {'name': ['controller', 'Heartbeat_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'heartbeat_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Heartbeat Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': 'mdi:wifi', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.IP_ADDRESS: {'name': ['controller', 'IP_Address'], 'level': logging.DEBUG, 'unit': '', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'ip_address_', 'fmt': '| string', 'name': 'IP Address', 'icon': WIFI, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': '| string + " s"', 'name': 'Polling Interval', 'icon': 'mdi:update', 'ent_cat': 'diagnostic'}}, # noqa: E501 Register.POLLING_INTERVAL: {'name': ['controller', 'Polling_Interval'], 'level': logging.DEBUG, 'unit': 's', 'ha': {'dev': 'controller', 'dev_cla': None, 'stat_cla': None, 'id': 'polling_intval_', 'fmt': FMT_STRING_SEC, 'name': 'Polling Interval', 'icon': UPDATE, 'ent_cat': 'diagnostic'}}, # noqa: E501
Register.SENSOR_LIST: {'name': ['controller', 'Sensor_List'], 'level': logging.INFO, 'unit': ''}, # noqa: E501
} }
@property @property

View File

@@ -14,6 +14,25 @@ else: # pragma: no cover
logger = logging.getLogger('msg') logger = logging.getLogger('msg')
def __hex_val(n, data, data_len):
line = ''
for j in range(n-16, n):
if j >= data_len:
break
line += '%02x ' % abs(data[j])
return line
def __asc_val(n, data, data_len):
line = ''
for j in range(n-16, n):
if j >= data_len:
break
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
line += '%c' % c
return line
def hex_dump_memory(level, info, data, data_len): def hex_dump_memory(level, info, data, data_len):
n = 0 n = 0
lines = [] lines = []
@@ -26,20 +45,9 @@ def hex_dump_memory(level, info, data, data_len):
line = ' ' line = ' '
line += '%04x | ' % (i) line += '%04x | ' % (i)
n += 16 n += 16
line += __hex_val(n, data, data_len)
for j in range(n-16, n):
if j >= data_len:
break
line += '%02x ' % abs(data[j])
line += ' ' * (3 * 16 + 9 - len(line)) + ' | ' line += ' ' * (3 * 16 + 9 - len(line)) + ' | '
line += __asc_val(n, data, data_len)
for j in range(n-16, n):
if j >= data_len:
break
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
line += '%c' % c
lines.append(line) lines.append(line)
tracer.log(level, '\n'.join(lines)) tracer.log(level, '\n'.join(lines))

View File

@@ -39,7 +39,7 @@ class Modbus():
'''Modbus function code: Write Single Register''' '''Modbus function code: Write Single Register'''
__crc_tab = [] __crc_tab = []
map = { mb_reg_mapping = {
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501 0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501 0x202c: {'reg': Register.OUTPUT_COEFFICIENT, 'fmt': '!H', 'ratio': 100/1024}, # noqa: E501
@@ -139,7 +139,7 @@ class Modbus():
if self.que.qsize() == 1: if self.que.qsize() == 1:
self.__send_next_from_que() self.__send_next_from_que()
def recv_req(self, buf: bytearray, def recv_req(self, buf: bytes,
rsp_handler: Callable[[None], None] = None) -> bool: rsp_handler: Callable[[None], None] = None) -> bool:
"""Add the received Modbus RTU request to the tx queue """Add the received Modbus RTU request to the tx queue
@@ -164,7 +164,7 @@ class Modbus():
return True return True
def recv_resp(self, info_db, buf: bytearray, node_id: str) -> \ def recv_resp(self, info_db, buf: bytes, node_id: str) -> \
Generator[tuple[str, bool, int | float | str], None, None]: Generator[tuple[str, bool, int | float | str], None, None]:
"""Generator which check and parse a received MODBUS response. """Generator which check and parse a received MODBUS response.
@@ -183,58 +183,18 @@ class Modbus():
# logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}') # logging.info(f'recv_resp: first byte modbus:{buf[0]} len:{len(buf)}')
self.node_id = node_id self.node_id = node_id
if not self.req_pend:
self.err = 5
return
if not self.__check_crc(buf):
logger.error(f'[{node_id}] Modbus resp: CRC error')
self.err = 1
return
if buf[0] != self.last_addr:
logger.info(f'[{node_id}] Modbus resp: Wrong addr {buf[0]}')
self.err = 2
return
fcode = buf[1] fcode = buf[1]
if fcode != self.last_fcode: data_available = self.last_addr == self.INV_ADDR and \
logger.info(f'[{node_id}] Modbus: Wrong fcode {fcode}' (fcode == 3 or fcode == 4)
f' != {self.last_fcode}')
self.err = 3 if self.__resp_error_check(buf, data_available):
return return
if self.last_addr == self.INV_ADDR and \
(fcode == 3 or fcode == 4): if data_available:
elmlen = buf[2] >> 1 elmlen = buf[2] >> 1
if elmlen != self.last_len:
logger.info(f'[{node_id}] Modbus: len error {elmlen}'
f' != {self.last_len}')
self.err = 4
return
first_reg = self.last_reg # save last_reg before sending next pdu first_reg = self.last_reg # save last_reg before sending next pdu
self.__stop_timer() # stop timer and send next pdu self.__stop_timer() # stop timer and send next pdu
yield from self.__process_data(info_db, buf, first_reg, elmlen)
for i in range(0, elmlen):
addr = first_reg+i
if addr in self.map:
row = self.map[addr]
info_id = row['reg']
fmt = row['fmt']
val = struct.unpack_from(fmt, buf, 3+2*i)
result = val[0]
if 'eval' in row:
result = eval(row['eval'])
if 'ratio' in row:
result = round(result * row['ratio'], 2)
keys, level, unit, must_incr = info_db._key_obj(info_id)
if keys:
name, update = info_db.update_db(keys, must_incr,
result)
yield keys[0], update, result
if update:
info_db.tracer.log(level,
f'[{node_id}] MODBUS: {name}'
f' : {result}{unit}')
else: else:
self.__stop_timer() self.__stop_timer()
@@ -243,6 +203,64 @@ class Modbus():
self.rsp_handler() self.rsp_handler()
self.__send_next_from_que() self.__send_next_from_que()
def __resp_error_check(self, buf: bytes, data_available: bool) -> bool:
'''Check the MODBUS response for errors, returns True if one accure'''
if not self.req_pend:
self.err = 5
return True
if not self.__check_crc(buf):
logger.error(f'[{self.node_id}] Modbus resp: CRC error')
self.err = 1
return True
if buf[0] != self.last_addr:
logger.info(f'[{self.node_id}] Modbus resp: Wrong addr {buf[0]}')
self.err = 2
return True
fcode = buf[1]
if fcode != self.last_fcode:
logger.info(f'[{self.node_id}] Modbus: Wrong fcode {fcode}'
f' != {self.last_fcode}')
self.err = 3
return True
if data_available:
elmlen = buf[2] >> 1
if elmlen != self.last_len:
logger.info(f'[{self.node_id}] Modbus: len error {elmlen}'
f' != {self.last_len}')
self.err = 4
return True
return False
def __get_value(self, buf: bytes, idx: int, row: dict):
'''get a value from the received buffer'''
val = struct.unpack_from(row['fmt'], buf, idx)
result = val[0]
if 'eval' in row:
result = eval(row['eval'])
if 'ratio' in row:
result = round(result * row['ratio'], 2)
return result
def __process_data(self, info_db, buf: bytes, first_reg, elmlen):
'''Generator over received registers, updates the db'''
for i in range(0, elmlen):
addr = first_reg+i
if addr in self.mb_reg_mapping:
row = self.mb_reg_mapping[addr]
info_id = row['reg']
keys, level, unit, must_incr = info_db._key_obj(info_id)
if keys:
result = self.__get_value(buf, 3+2*i, row)
name, update = info_db.update_db(keys, must_incr,
result)
yield keys[0], update, result
if update:
info_db.tracer.log(level,
f'[{self.node_id}] MODBUS: {name}'
f' : {result}{unit}')
''' '''
MODBUS response timer MODBUS response timer
''' '''
@@ -302,11 +320,11 @@ class Modbus():
''' '''
Helper function for CRC-16 handling Helper function for CRC-16 handling
''' '''
def __check_crc(self, msg: bytearray) -> bool: def __check_crc(self, msg: bytes) -> bool:
'''Check CRC-16 and returns True if valid''' '''Check CRC-16 and returns True if valid'''
return 0 == self.__calc_crc(msg) return 0 == self.__calc_crc(msg)
def __calc_crc(self, buffer: bytearray) -> int: def __calc_crc(self, buffer: bytes) -> int:
'''Build CRC-16 for buffer and returns it''' '''Build CRC-16 for buffer and returns it'''
crc = CRC_INIT crc = CRC_INIT

View File

@@ -3,7 +3,6 @@ import traceback
import asyncio import asyncio
from config import Config from config import Config
# import gc
from gen3plus.inverter_g3p import InverterG3P from gen3plus.inverter_g3p import InverterG3P
logger = logging.getLogger('conn') logger = logging.getLogger('conn')
@@ -66,11 +65,14 @@ class ModbusTcp():
logging.debug(f'Inv-conn:{error}') logging.debug(f'Inv-conn:{error}')
except OSError as error: except OSError as error:
if error.errno == 113:
logging.debug(f'os-error:{error}')
else:
logging.info(f'os-error: {error}') logging.info(f'os-error: {error}')
except Exception: except Exception:
logging.error( logging.error(
f"ModbusTcpCreate: Exception for {(host,port)}:\n" f"ModbusTcpCreate: Exception for {(host, port)}:\n"
f"{traceback.format_exc()}") f"{traceback.format_exc()}")
await asyncio.sleep(10) await asyncio.sleep(10)

View File

@@ -2,26 +2,40 @@ import asyncio
import logging import logging
import aiomqtt import aiomqtt
import traceback import traceback
from modbus import Modbus if __name__ == "app.src.mqtt":
from messages import Message from app.src.modbus import Modbus
from config import Config from app.src.messages import Message
from singleton import Singleton from app.src.config import Config
from app.src.singleton import Singleton
else: # pragma: no cover
from modbus import Modbus
from messages import Message
from config import Config
from singleton import Singleton
logger_mqtt = logging.getLogger('mqtt') logger_mqtt = logging.getLogger('mqtt')
class Mqtt(metaclass=Singleton): class Mqtt(metaclass=Singleton):
__client = None __client = None
__cb_MqttIsUp = None __cb_mqtt_is_up = None
def __init__(self, cb_MqttIsUp): def __init__(self, cb_mqtt_is_up):
logger_mqtt.debug('MQTT: __init__') logger_mqtt.debug('MQTT: __init__')
if cb_MqttIsUp: if cb_mqtt_is_up:
self.__cb_MqttIsUp = cb_MqttIsUp self.__cb_mqtt_is_up = cb_mqtt_is_up
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
self.task = loop.create_task(self.__loop()) self.task = loop.create_task(self.__loop())
self.ha_restarts = 0 self.ha_restarts = 0
ha = Config.get('ha')
self.ha_status_topic = f"{ha['auto_conf_prefix']}/status"
self.mb_rated_topic = f"{ha['entity_prefix']}/+/rated_load"
self.mb_out_coeff_topic = f"{ha['entity_prefix']}/+/out_coeff"
self.mb_reads_topic = f"{ha['entity_prefix']}/+/modbus_read_regs"
self.mb_inputs_topic = f"{ha['entity_prefix']}/+/modbus_read_inputs"
self.mb_at_cmd_topic = f"{ha['entity_prefix']}/+/at_cmd"
@property @property
def ha_restarts(self): def ha_restarts(self):
return self._ha_restarts return self._ha_restarts
@@ -49,7 +63,6 @@ class Mqtt(metaclass=Singleton):
async def __loop(self) -> None: async def __loop(self) -> None:
mqtt = Config.get('mqtt') mqtt = Config.get('mqtt')
ha = Config.get('ha')
logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:' logger_mqtt.info(f'start MQTT: host:{mqtt["host"]} port:'
f'{mqtt["port"]} ' f'{mqtt["port"]} '
f'user:{mqtt["user"]}') f'user:{mqtt["user"]}')
@@ -59,66 +72,24 @@ class Mqtt(metaclass=Singleton):
password=mqtt['passwd']) password=mqtt['passwd'])
interval = 5 # Seconds interval = 5 # Seconds
ha_status_topic = f"{ha['auto_conf_prefix']}/status"
mb_rated_topic = "tsun/+/rated_load" # fixme
mb_out_coeff_topic = "tsun/+/out_coeff" # fixme
mb_reads_topic = "tsun/+/modbus_read_regs" # fixme
mb_inputs_topic = "tsun/+/modbus_read_inputs" # fixme
mb_at_cmd_topic = "tsun/+/at_cmd" # fixme
while True: while True:
try: try:
async with self.__client: async with self.__client:
logger_mqtt.info('MQTT broker connection established') logger_mqtt.info('MQTT broker connection established')
if self.__cb_MqttIsUp: if self.__cb_mqtt_is_up:
await self.__cb_MqttIsUp() await self.__cb_mqtt_is_up()
# async with self.__client.messages() as messages: await self.__client.subscribe(self.ha_status_topic)
await self.__client.subscribe(ha_status_topic) await self.__client.subscribe(self.mb_rated_topic)
await self.__client.subscribe(mb_rated_topic) await self.__client.subscribe(self.mb_out_coeff_topic)
await self.__client.subscribe(mb_out_coeff_topic) await self.__client.subscribe(self.mb_reads_topic)
await self.__client.subscribe(mb_reads_topic) await self.__client.subscribe(self.mb_inputs_topic)
await self.__client.subscribe(mb_inputs_topic) await self.__client.subscribe(self.mb_at_cmd_topic)
await self.__client.subscribe(mb_at_cmd_topic)
async for message in self.__client.messages: async for message in self.__client.messages:
if message.topic.matches(ha_status_topic): await self.dispatch_msg(message)
status = message.payload.decode("UTF-8")
logger_mqtt.info('Home-Assistant Status:'
f' {status}')
if status == 'online':
self.ha_restarts += 1
await self.__cb_MqttIsUp()
if message.topic.matches(mb_rated_topic):
await self.modbus_cmd(message,
Modbus.WRITE_SINGLE_REG,
1, 0x2008)
if message.topic.matches(mb_out_coeff_topic):
payload = message.payload.decode("UTF-8")
val = round(float(payload) * 1024/100)
if val < 0 or val > 1024:
logger_mqtt.error('out_coeff: value must be in'
'the range 0..100,'
f' got: {payload}')
else:
await self.modbus_cmd(message,
Modbus.WRITE_SINGLE_REG,
0, 0x202c, val)
if message.topic.matches(mb_reads_topic):
await self.modbus_cmd(message,
Modbus.READ_REGS, 2)
if message.topic.matches(mb_inputs_topic):
await self.modbus_cmd(message,
Modbus.READ_INPUTS, 2)
if message.topic.matches(mb_at_cmd_topic):
await self.at_cmd(message)
except aiomqtt.MqttError: except aiomqtt.MqttError:
if Config.is_default('mqtt'): if Config.is_default('mqtt'):
@@ -142,40 +113,70 @@ class Mqtt(metaclass=Singleton):
f"Exception:\n" f"Exception:\n"
f"{traceback.format_exc()}") f"{traceback.format_exc()}")
async def dispatch_msg(self, message):
if message.topic.matches(self.ha_status_topic):
status = message.payload.decode("UTF-8")
logger_mqtt.info('Home-Assistant Status:'
f' {status}')
if status == 'online':
self.ha_restarts += 1
await self.__cb_mqtt_is_up()
if message.topic.matches(self.mb_rated_topic):
await self.modbus_cmd(message,
Modbus.WRITE_SINGLE_REG,
1, 0x2008)
if message.topic.matches(self.mb_out_coeff_topic):
payload = message.payload.decode("UTF-8")
try:
val = round(float(payload) * 1024/100)
if val < 0 or val > 1024:
logger_mqtt.error('out_coeff: value must be in'
'the range 0..100,'
f' got: {payload}')
else:
await self.modbus_cmd(message,
Modbus.WRITE_SINGLE_REG,
0, 0x202c, val)
except Exception:
pass
if message.topic.matches(self.mb_reads_topic):
await self.modbus_cmd(message,
Modbus.READ_REGS, 2)
if message.topic.matches(self.mb_inputs_topic):
await self.modbus_cmd(message,
Modbus.READ_INPUTS, 2)
if message.topic.matches(self.mb_at_cmd_topic):
await self.at_cmd(message)
def each_inverter(self, message, func_name: str): def each_inverter(self, message, func_name: str):
topic = str(message.topic) topic = str(message.topic)
node_id = topic.split('/')[1] + '/' node_id = topic.split('/')[1] + '/'
found = False
for m in Message: for m in Message:
if m.server_side and (m.node_id == node_id): if m.server_side and (m.node_id == node_id):
found = True
logger_mqtt.debug(f'Found: {node_id}') logger_mqtt.debug(f'Found: {node_id}')
fnc = getattr(m, func_name, None) fnc = getattr(m, func_name, None)
if callable(fnc): if callable(fnc):
yield fnc yield fnc
else: else:
logger_mqtt.warning(f'Cmd not supported by: {node_id}') logger_mqtt.warning(f'Cmd not supported by: {node_id}')
break
if not found: else:
logger_mqtt.warning(f'Node_id: {node_id} not found') logger_mqtt.warning(f'Node_id: {node_id} not found')
async def modbus_cmd(self, message, func, params=0, addr=0, val=0): async def modbus_cmd(self, message, func, params=0, addr=0, val=0):
topic = str(message.topic)
node_id = topic.split('/')[1] + '/'
# refactor into a loop over a table
payload = message.payload.decode("UTF-8") payload = message.payload.decode("UTF-8")
logger_mqtt.info(f'MODBUS via MQTT: {topic} = {payload}') for fnc in self.each_inverter(message, "send_modbus_cmd"):
for m in Message:
if m.server_side and (m.node_id == node_id):
logger_mqtt.debug(f'Found: {node_id}')
fnc = getattr(m, "send_modbus_cmd", None)
res = payload.split(',') res = payload.split(',')
if params > 0 and params != len(res): if params > 0 and params != len(res):
logger_mqtt.error(f'Parameter expected: {params}, ' logger_mqtt.error(f'Parameter expected: {params}, '
f'got: {len(res)}') f'got: {len(res)}')
return return
if callable(fnc):
if params == 1: if params == 1:
val = int(payload) val = int(payload)
elif params == 2: elif params == 2:

View File

@@ -1,9 +1,14 @@
from weakref import WeakValueDictionary
class Singleton(type): class Singleton(type):
_instances = {} _instances = WeakValueDictionary()
def __call__(cls, *args, **kwargs): def __call__(cls, *args, **kwargs):
# logger_mqtt.debug('singleton: __call__') # logger_mqtt.debug('singleton: __call__')
if cls not in cls._instances: if cls not in cls._instances:
cls._instances[cls] = super(Singleton, instance = super(Singleton,
cls).__call__(*args, **kwargs) cls).__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls] return cls._instances[cls]

View File

@@ -7,11 +7,11 @@ class TstConfig(Config):
@classmethod @classmethod
def set(cls, cnf): def set(cls, cnf):
cls.config = cnf cls.act_config = cnf
@classmethod @classmethod
def _read_config_file(cls) -> dict: def _read_config_file(cls) -> dict:
return cls.config return cls.act_config
def test_empty_config(): def test_empty_config():
@@ -30,7 +30,7 @@ def test_default_config():
validated = Config.conf_schema.validate(cnf) validated = Config.conf_schema.validate(cnf)
except Exception: except Exception:
assert False assert False
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': False, 'monitor_sn': 0, 'suggested_area': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}} assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': False, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': '', 'sensor_list': 688}}}
def test_full_config(): def test_full_config():
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
@@ -40,13 +40,13 @@ def test_full_config():
'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''},
'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'},
'inverters': {'allow_all': True, 'inverters': {'allow_all': True,
'R170000000000001': {'modbus_polling': True, 'node_id': '', 'suggested_area': '', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}}, 'R170000000000001': {'modbus_polling': True, 'node_id': '', 'sensor_list': 0, 'suggested_area': '', 'pv1': {'type': 'type1', 'manufacturer': 'man1'}, 'pv2': {'type': 'type2', 'manufacturer': 'man2'}, 'pv3': {'type': 'type3', 'manufacturer': 'man3'}},
'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}} 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'sensor_list': 0x1511, 'suggested_area': ''}}}
try: try:
validated = Config.conf_schema.validate(cnf) validated = Config.conf_schema.validate(cnf)
except Exception: except Exception:
assert False assert False
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'pv1': {'manufacturer': 'man1','type': 'type1'},'pv2': {'manufacturer': 'man2','type': 'type2'},'pv3': {'manufacturer': 'man3','type': 'type3'}, 'suggested_area': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': ''}}} assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'pv1': {'manufacturer': 'man1','type': 'type1'},'pv2': {'manufacturer': 'man2','type': 'type2'},'pv3': {'manufacturer': 'man3','type': 'type3'}, 'suggested_area': '', 'sensor_list': 0}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'node_id': '', 'suggested_area': '', 'sensor_list': 5393}}}
def test_mininum_config(): def test_mininum_config():
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
@@ -63,7 +63,7 @@ def test_mininum_config():
validated = Config.conf_schema.validate(cnf) validated = Config.conf_schema.validate(cnf)
except Exception: except Exception:
assert False assert False
assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'suggested_area': ''}}} assert validated == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'node_id': '', 'modbus_polling': True, 'monitor_sn': 0, 'suggested_area': '', 'sensor_list': 688}}}
def test_read_empty(): def test_read_empty():
cnf = {} cnf = {}
@@ -71,7 +71,7 @@ def test_read_empty():
err = TstConfig.read('app/config/') err = TstConfig.read('app/config/')
assert err == None assert err == None
cnf = TstConfig.get() cnf = TstConfig.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}} assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
defcnf = TstConfig.def_config.get('solarman') defcnf = TstConfig.def_config.get('solarman')
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000} assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
@@ -93,7 +93,7 @@ def test_read_cnf1():
err = TstConfig.read('app/config/') err = TstConfig.read('app/config/')
assert err == None assert err == None
cnf = TstConfig.get() cnf = TstConfig.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}} assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
cnf = TstConfig.get('solarman') cnf = TstConfig.get('solarman')
assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000} assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}
defcnf = TstConfig.def_config.get('solarman') defcnf = TstConfig.def_config.get('solarman')
@@ -106,7 +106,7 @@ def test_read_cnf2():
err = TstConfig.read('app/config/') err = TstConfig.read('app/config/')
assert err == None assert err == None
cnf = TstConfig.get() cnf = TstConfig.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}} assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
assert True == TstConfig.is_default('solarman') assert True == TstConfig.is_default('solarman')
def test_read_cnf3(): def test_read_cnf3():
@@ -123,7 +123,7 @@ def test_read_cnf4():
err = TstConfig.read('app/config/') err = TstConfig.read('app/config/')
assert err == None assert err == None
cnf = TstConfig.get() cnf = TstConfig.get()
assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': ''}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': ''}}} assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': {'allow_all': True, 'R170000000000001': {'suggested_area': '', 'modbus_polling': False, 'monitor_sn': 0, 'node_id': '', 'sensor_list': 688}, 'Y170000000000001': {'modbus_polling': True, 'monitor_sn': 2000000000, 'suggested_area': '', 'node_id': '', 'sensor_list': 688}}}
assert False == TstConfig.is_default('solarman') assert False == TstConfig.is_default('solarman')
def test_read_cnf5(): def test_read_cnf5():

View File

@@ -4,7 +4,7 @@ from app.src.infos import Register, ClrAtMidnight
from app.src.gen3.infos_g3 import InfosG3 from app.src.gen3.infos_g3 import InfosG3
@pytest.fixture @pytest.fixture
def ContrDataSeq(): # Get Time Request message def contr_data_seq(): # Get Time Request message
msg = b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f' msg = b'\x00\x00\x00\x15\x00\x09\x2b\xa8\x54\x10\x52\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x30\x36\x00\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f'
msg += b'\x6e\x00\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54' msg += b'\x6e\x00\x09\x2f\x90\x54\x0b\x52\x53\x57\x2d\x31\x2d\x31\x30\x30\x30\x31\x00\x09\x5a\x88\x54\x0f\x74\x2e\x72\x61\x79\x6d\x6f\x6e\x69\x6f\x74\x2e\x63\x6f\x6d\x00\x09\x5a\xec\x54'
msg += b'\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00' msg += b'\x1c\x6c\x6f\x67\x67\x65\x72\x2e\x74\x61\x6c\x65\x6e\x74\x2d\x6d\x6f\x6e\x69\x74\x6f\x72\x69\x6e\x67\x2e\x63\x6f\x6d\x00\x0d\x00\x20\x49\x00\x00\x00\x01\x00\x0c\x35\x00\x49\x00'
@@ -14,7 +14,7 @@ def ContrDataSeq(): # Get Time Request message
return msg return msg
@pytest.fixture @pytest.fixture
def Contr2DataSeq(): # Get Time Request message def contr2_data_seq(): # Get Time Request message
msg = b'\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52' msg = b'\x00\x00\x00\x39\x00\x09\x2b\xa8\x54\x10\x52'
msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x32\x30\x00' msg += b'\x53\x57\x5f\x34\x30\x30\x5f\x56\x31\x2e\x30\x30\x2e\x32\x30\x00'
msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54' msg += b'\x09\x27\xc0\x54\x06\x52\x61\x79\x6d\x6f\x6e\x00\x09\x2f\x90\x54'
@@ -94,19 +94,19 @@ def Contr2DataSeq(): # Get Time Request message
return msg return msg
@pytest.fixture @pytest.fixture
def InvDataSeq(): # Data indication from the controller def inv_data_seq(): # Data indication from the controller
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28' msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x54\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
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' 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 return msg
@pytest.fixture @pytest.fixture
def InvalidDataSeq(): # Data indication from the controller def invalid_data_seq(): # Data indication from the controller
msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x64\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28' msg = b'\x00\x00\x00\x06\x00\x00\x00\x0a\x54\x08\x4d\x69\x63\x72\x6f\x69\x6e\x76\x00\x00\x00\x14\x64\x04\x54\x53\x55\x4e\x00\x00\x00\x1E\x54\x07\x56\x35\x2e\x30\x2e\x31\x31\x00\x00\x00\x28'
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' 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 return msg
@pytest.fixture @pytest.fixture
def InvDataSeq2(): # Data indication from the controller def inv_data_seq2(): # Data indication from the controller
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\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\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\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'
@@ -141,7 +141,7 @@ def InvDataSeq2(): # Data indication from the controller
return msg return msg
@pytest.fixture @pytest.fixture
def InvDataNew(): # Data indication from DSP V5.0.17 def inv_data_new(): # Data indication from DSP V5.0.17
msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00' msg = b'\x00\x00\x00\xa3\x00\x00\x00\x00\x53\x00\x00'
msg += b'\x00\x00\x00\x80\x53\x00\x00\x00\x00\x01\x04\x53\x00\x00\x00\x00' msg += b'\x00\x00\x00\x80\x53\x00\x00\x00\x00\x01\x04\x53\x00\x00\x00\x00'
msg += b'\x01\x90\x41\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00' msg += b'\x01\x90\x41\x00\x00\x01\x91\x53\x00\x00\x00\x00\x01\x90\x53\x00'
@@ -217,7 +217,7 @@ def InvDataNew(): # Data indication from DSP V5.0.17
return msg return msg
@pytest.fixture @pytest.fixture
def InvDataSeq2_Zero(): # Data indication from the controller def inv_data_seq2_zero(): # Data indication from the controller
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\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\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\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'
@@ -252,37 +252,37 @@ def InvDataSeq2_Zero(): # Data indication from the controller
return msg return msg
def test_parse_control(ContrDataSeq): def test_parse_control(contr_data_seq):
i = InfosG3() i = InfosG3()
for key, result in i.parse (ContrDataSeq): for key, result in i.parse (contr_data_seq):
pass pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps( assert json.dumps(i.db) == json.dumps(
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}}) {"collector": {"Collector_Fw_Version": "RSW_400_V1.00.06", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 100, "Power_On_Time": 29, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
def test_parse_control2(Contr2DataSeq): def test_parse_control2(contr2_data_seq):
i = InfosG3() i = InfosG3()
for key, result in i.parse (Contr2DataSeq): for key, result in i.parse (contr2_data_seq):
pass pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps( assert json.dumps(i.db) == json.dumps(
{"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}}) {"collector": {"Collector_Fw_Version": "RSW_400_V1.00.20", "Chip_Type": "Raymon", "Chip_Model": "RSW-1-10001", "Trace_URL": "t.raymoniot.com", "Logger_URL": "logger.talent-monitoring.com"}, "controller": {"Collect_Interval": 1, "Signal_Strength": 16, "Power_On_Time": 334, "Communication_Type": 1, "Connect_Count": 1, "Data_Up_Interval": 300}})
def test_parse_inverter(InvDataSeq): def test_parse_inverter(inv_data_seq):
i = InfosG3() i = InfosG3()
for key, result in i.parse (InvDataSeq): for key, result in i.parse (inv_data_seq):
pass pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps( assert json.dumps(i.db) == json.dumps(
{"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}}) {"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq): def test_parse_cont_and_invert(contr_data_seq, inv_data_seq):
i = InfosG3() i = InfosG3()
for key, result in i.parse (ContrDataSeq): for key, result in i.parse (contr_data_seq):
pass pass # side effect in calling i.parse()
for key, result in i.parse (InvDataSeq): for key, result in i.parse (inv_data_seq):
pass pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps( assert json.dumps(i.db) == json.dumps(
{ {
@@ -290,7 +290,7 @@ def test_parse_cont_and_invert(ContrDataSeq, InvDataSeq):
"inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}}) "inverter": {"Product_Name": "Microinv", "Manufacturer": "TSUN", "Version": "V5.0.11", "Serial_Number": "T170000000000001", "Equipment_Model": "TSOL-MS600"}})
def test_build_ha_conf1(ContrDataSeq): def test_build_ha_conf1(contr_data_seq):
i = InfosG3() i = InfosG3()
i.static_init() # initialize counter i.static_init() # initialize counter
@@ -346,14 +346,14 @@ def test_build_ha_conf1(ContrDataSeq):
assert tests==5 assert tests==5
def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2): def test_build_ha_conf2(contr_data_seq, inv_data_seq, inv_data_seq2):
i = InfosG3() i = InfosG3()
for key, result in i.parse (ContrDataSeq): for key, result in i.parse (contr_data_seq):
pass pass # side effect in calling i.parse()
for key, result in i.parse (InvDataSeq): for key, result in i.parse (inv_data_seq):
pass pass # side effect in calling i.parse()
for key, result in i.parse (InvDataSeq2): for key, result in i.parse (inv_data_seq2):
pass pass # side effect in calling i.parse()
tests = 0 tests = 0
for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'): for d_json, comp, node_id, id in i.ha_confs(ha_prfx="tsun/", node_id="garagendach/", snr='123', sug_area = 'roof'):
@@ -384,10 +384,10 @@ def test_build_ha_conf2(ContrDataSeq, InvDataSeq, InvDataSeq2):
tests +=1 tests +=1
assert tests==5 assert tests==5
def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero): def test_must_incr_total(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3() i = InfosG3()
tests = 0 tests = 0
for key, update in i.parse (InvDataSeq2): for key, update in i.parse (inv_data_seq2):
if key == 'total' or key == 'inverter' or key == 'env': if key == 'total' or key == 'inverter' or key == 'env':
assert update == True assert update == True
tests +=1 tests +=1
@@ -396,7 +396,7 @@ def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}}) assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23}) assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 23})
tests = 0 tests = 0
for key, update in i.parse (InvDataSeq2): for key, update in i.parse (inv_data_seq2):
if key == 'total': if key == 'total':
assert update == False assert update == False
tests +=1 tests +=1
@@ -411,7 +411,7 @@ def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2}) assert json.dumps(i.db['inverter']) == json.dumps({"Rated_Power": 600, "No_Inputs": 2})
tests = 0 tests = 0
for key, update in i.parse (InvDataSeq2_Zero): for key, update in i.parse (inv_data_seq2_zero):
if key == 'total': if key == 'total':
assert update == False assert update == False
tests +=1 tests +=1
@@ -424,10 +424,10 @@ def test_must_incr_total(InvDataSeq2, InvDataSeq2_Zero):
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}}) assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 0.0, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 0.0, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0}) assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero): def test_must_incr_total2(inv_data_seq2, inv_data_seq2_zero):
i = InfosG3() i = InfosG3()
tests = 0 tests = 0
for key, update in i.parse (InvDataSeq2_Zero): for key, update in i.parse (inv_data_seq2_zero):
if key == 'total': if key == 'total':
assert update == False assert update == False
tests +=1 tests +=1
@@ -441,7 +441,7 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0}) assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
tests = 0 tests = 0
for key, update in i.parse (InvDataSeq2_Zero): for key, update in i.parse (inv_data_seq2_zero):
if key == 'total': if key == 'total':
assert update == False assert update == False
tests +=1 tests +=1
@@ -455,7 +455,7 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0}) assert json.dumps(i.db['env']) == json.dumps({"Inverter_Temp": 0})
tests = 0 tests = 0
for key, update in i.parse (InvDataSeq2): for key, update in i.parse (inv_data_seq2):
if key == 'total': if key == 'total':
assert update == True assert update == True
tests +=1 tests +=1
@@ -467,10 +467,10 @@ def test_must_incr_total2(InvDataSeq2, InvDataSeq2_Zero):
assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36}) assert json.dumps(i.db['total']) == json.dumps({'Daily_Generation': 1.7, 'Total_Generation': 17.36})
assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}}) assert json.dumps(i.db['input']) == json.dumps({"pv1": {"Voltage": 33.6, "Current": 1.91, "Power": 64.5, "Daily_Generation": 1.08, "Total_Generation": 9.74}, "pv2": {"Voltage": 33.5, "Current": 1.36, "Power": 45.7, "Daily_Generation": 0.62, "Total_Generation": 7.62}, "pv3": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}, "pv4": {"Voltage": 0.0, "Current": 0.0, "Power": 0.0}})
def test_new_data_types(InvDataNew): def test_new_data_types(inv_data_new):
i = InfosG3() i = InfosG3()
tests = 0 tests = 0
for key, update in i.parse (InvDataNew): for key, update in i.parse (inv_data_new):
if key == 'events': if key == 'events':
tests +=1 tests +=1
elif key == 'inverter': elif key == 'inverter':
@@ -487,7 +487,7 @@ def test_new_data_types(InvDataNew):
assert json.dumps(i.db['input']) == json.dumps({"pv1": {}}) assert json.dumps(i.db['input']) == json.dumps({"pv1": {}})
assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0}) assert json.dumps(i.db['events']) == json.dumps({"401_": 0, "404_": 0, "405_": 0, "408_": 0, "409_No_Utility": 0, "406_": 0, "416_": 0})
def test_invalid_data_type(InvalidDataSeq): def test_invalid_data_type(invalid_data_seq):
i = InfosG3() i = InfosG3()
i.static_init() # initialize counter i.static_init() # initialize counter
@@ -495,8 +495,8 @@ def test_invalid_data_type(InvalidDataSeq):
assert val == 0 assert val == 0
for key, result in i.parse (InvalidDataSeq): for key, result in i.parse (invalid_data_seq):
pass pass # side effect in calling i.parse()
assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}}) assert json.dumps(i.db) == json.dumps({"inverter": {"Product_Name": "Microinv"}})
val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter val = i.dev_value(Register.INVALID_DATA_TYPE) # check invalid data type counter

View File

@@ -70,7 +70,7 @@ def test_parse_4110(device_data: bytes):
pass # side effect is calling generator i.parse() pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({ assert json.dumps(i.db) == json.dumps({
'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": "192.168.80.49"}, 'controller': {"Data_Up_Interval": 300, "Collect_Interval": 1, "Heartbeat_Interval": 120, "Signal_Strength": 100, "IP_Address": "192.168.80.49", "Sensor_List": "02b0"},
'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"}, 'collector': {"Chip_Model": "LSW5BLE_17_02B0_1.05", "Collector_Fw_Version": "V1.1.00.0B"},
}) })
@@ -82,7 +82,7 @@ def test_parse_4210(inverter_data: bytes):
pass # side effect is calling generator i.parse() pass # side effect is calling generator i.parse()
assert json.dumps(i.db) == json.dumps({ assert json.dumps(i.db) == json.dumps({
"controller": {"Power_On_Time": 2051}, "controller": {"Sensor_List": "02b0", "Power_On_Time": 2051},
"inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000}, "inverter": {"Serial_Number": "Y17E00000000000E", "Version": "V4.0.10", "Rated_Power": 600, "Max_Designed_Power": 2000},
"env": {"Inverter_Status": 1, "Inverter_Temp": 14}, "env": {"Inverter_Status": 1, "Inverter_Temp": 14},
"grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8}, "grid": {"Voltage": 224.8, "Current": 0.73, "Frequency": 50.05, "Output_Power": 165.8},

View File

@@ -366,8 +366,8 @@ async def test_timeout():
def test_recv_unknown_data(): def test_recv_unknown_data():
'''Receive a response with an unknwon register''' '''Receive a response with an unknwon register'''
mb = ModbusTestHelper() mb = ModbusTestHelper()
assert 0x9000 not in mb.map assert 0x9000 not in mb.mb_reg_mapping
mb.map[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1} mb.mb_reg_mapping[0x9000] = {'reg': Register.TEST_REG1, 'fmt': '!H', 'ratio': 1}
mb.build_msg(1,3,0x9000,2) mb.build_msg(1,3,0x9000,2)
@@ -379,7 +379,7 @@ def test_recv_unknown_data():
assert 0 == call assert 0 == call
assert not mb.req_pend assert not mb.req_pend
del mb.map[0x9000] del mb.mb_reg_mapping[0x9000]
def test_close(): def test_close():
'''Check queue handling for build_msg() calls''' '''Check queue handling for build_msg() calls'''

250
app/tests/test_mqtt.py Normal file
View File

@@ -0,0 +1,250 @@
# test_with_pytest.py
import pytest
import asyncio
import aiomqtt
import logging
from mock import patch, Mock
from app.src.mqtt import Mqtt
from app.src.modbus import Modbus
from app.src.gen3plus.solarman_v5 import SolarmanV5
from app.src.config import Config
pytest_plugins = ('pytest_asyncio',)
@pytest.fixture(scope="module")
def test_port():
return 1883
@pytest.fixture(scope="module")
def test_hostname():
# if getenv("GITHUB_ACTIONS") == "true":
# return 'mqtt'
# else:
return 'test.mosquitto.org'
@pytest.fixture
def config_mqtt_conn(test_hostname, test_port):
Config.act_config = {'mqtt':{'host': test_hostname, 'port': test_port, 'user': '', 'passwd': ''},
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
}
@pytest.fixture
def config_no_conn(test_port):
Config.act_config = {'mqtt':{'host': "", 'port': test_port, 'user': '', 'passwd': ''},
'ha':{'auto_conf_prefix': 'homeassistant','discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun'}
}
@pytest.fixture
def spy_at_cmd():
conn = SolarmanV5(server_side=True, client_mode= False)
conn.node_id = 'inv_2/'
with patch.object(conn, 'send_at_cmd', wraps=conn.send_at_cmd) as wrapped_conn:
yield wrapped_conn
conn.close()
@pytest.fixture
def spy_modbus_cmd():
conn = SolarmanV5(server_side=True, client_mode= False)
conn.node_id = 'inv_1/'
with patch.object(conn, 'send_modbus_cmd', wraps=conn.send_modbus_cmd) as wrapped_conn:
yield wrapped_conn
conn.close()
@pytest.fixture
def spy_modbus_cmd_client():
conn = SolarmanV5(server_side=False, client_mode= False)
conn.node_id = 'inv_1/'
with patch.object(conn, 'send_modbus_cmd', wraps=conn.send_modbus_cmd) as wrapped_conn:
yield wrapped_conn
conn.close()
def test_native_client(test_hostname, test_port):
"""Sanity check: Make sure the paho-mqtt client can connect to the test
MQTT server.
"""
import paho.mqtt.client as mqtt
import threading
c = mqtt.Client()
c.loop_start()
try:
# Just make sure the client connects successfully
on_connect = threading.Event()
c.on_connect = Mock(side_effect=lambda *_: on_connect.set())
c.connect_async(test_hostname, test_port)
assert on_connect.wait(5)
finally:
c.loop_stop()
@pytest.mark.asyncio
async def test_mqtt_no_config(config_no_conn):
_ = config_no_conn
assert asyncio.get_running_loop()
on_connect = asyncio.Event()
async def cb():
on_connect.set()
try:
m = Mqtt(cb)
assert m.task
await asyncio.sleep(1)
assert not on_connect.is_set()
try:
await m.publish('homeassistant/status', 'online')
assert False
except Exception:
pass
except TimeoutError:
assert False
finally:
await m.close()
@pytest.mark.asyncio
async def test_mqtt_connection(config_mqtt_conn):
_ = config_mqtt_conn
assert asyncio.get_running_loop()
on_connect = asyncio.Event()
async def cb():
on_connect.set()
try:
m = Mqtt(cb)
assert m.task
assert await asyncio.wait_for(on_connect.wait(), 5)
# await asyncio.sleep(1)
assert 0 == m.ha_restarts
await m.publish('homeassistant/status', 'online')
except TimeoutError:
assert False
finally:
await m.close()
await m.publish('homeassistant/status', 'online')
@pytest.mark.asyncio
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
_ = config_mqtt_conn
spy = spy_modbus_cmd
try:
m = Mqtt(None)
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x2008, 2, logging.INFO)
spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'100', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 1024, logging.INFO)
spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'50', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.WRITE_SINGLE_REG, 0x202c, 512, logging.INFO)
spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.READ_REGS, 0x3000, 10, logging.INFO)
spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_inputs', payload= b'0x3000, 10', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_awaited_once_with(Modbus.READ_INPUTS, 0x3000, 10, logging.INFO)
finally:
await m.close()
@pytest.mark.asyncio
async def test_msg_dispatch_err(config_mqtt_conn, spy_modbus_cmd):
_ = config_mqtt_conn
spy = spy_modbus_cmd
try:
m = Mqtt(None)
# test out of range param
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'-1', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_not_called()
# test unknown node_id
spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_2/out_coeff', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_not_called()
# test invalid fload param
spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/out_coeff', payload= b'2, 3', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_not_called()
spy.reset_mock()
msg = aiomqtt.Message(topic= 'tsun/inv_1/modbus_read_regs', payload= b'0x3000, 10, 7', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_not_called()
finally:
await m.close()
@pytest.mark.asyncio
async def test_msg_ignore_client_conn(config_mqtt_conn, spy_modbus_cmd_client):
'''don't call function if connnection is not in server mode'''
_ = config_mqtt_conn
spy = spy_modbus_cmd_client
try:
m = Mqtt(None)
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_not_called()
finally:
await m.close()
@pytest.mark.asyncio
async def test_ha_reconnect(config_mqtt_conn):
_ = config_mqtt_conn
on_connect = asyncio.Event()
async def cb():
on_connect.set()
try:
m = Mqtt(cb)
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'offline', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
assert not on_connect.is_set()
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
assert on_connect.is_set()
finally:
await m.close()
@pytest.mark.asyncio
async def test_ignore_unknown_func(config_mqtt_conn):
'''don't dispatch for unknwon function names'''
_ = config_mqtt_conn
try:
m = Mqtt(None)
msg = aiomqtt.Message(topic= 'tsun/inv_1/rated_load', payload= b'2', qos= 0, retain = False, mid= 0, properties= None)
for _ in m.each_inverter(msg, 'unkown_fnc'):
assert False
finally:
await m.close()
@pytest.mark.asyncio
async def test_at_cmd_dispatch(config_mqtt_conn, spy_at_cmd):
_ = config_mqtt_conn
spy = spy_at_cmd
try:
m = Mqtt(None)
msg = aiomqtt.Message(topic= 'tsun/inv_2/at_cmd', payload= b'AT+', qos= 0, retain = False, mid= 0, properties= None)
await m.dispatch_msg(msg)
spy.assert_awaited_once_with('AT+')
finally:
await m.close()

View File

@@ -0,0 +1,18 @@
# test_with_pytest.py
import pytest
from app.src.singleton import Singleton
class Test(metaclass=Singleton):
def __init__(self):
pass # is a dummy test class
def test_singleton_metaclass():
a = Test()
assert 1 == len(Singleton._instances)
b = Test()
assert 1 == len(Singleton._instances)
assert a is b
del a
assert 1 == len(Singleton._instances)
del b
assert 0 == len(Singleton._instances)

View File

@@ -3,11 +3,12 @@ import struct
import time import time
import asyncio import asyncio
import logging import logging
from math import isclose
from app.src.gen3plus.solarman_v5 import SolarmanV5 from app.src.gen3plus.solarman_v5 import SolarmanV5
from app.src.config import Config from app.src.config import Config
from app.src.infos import Infos, Register from app.src.infos import Infos, Register
from app.src.modbus import Modbus from app.src.modbus import Modbus
from app.src.messages import State from app.src.messages import State, Message
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)
@@ -41,7 +42,7 @@ class MemoryStream(SolarmanV5):
super().__init__(server_side, client_mode=False) super().__init__(server_side, client_mode=False)
if server_side: if server_side:
self.mb.timeout = 0.4 # overwrite for faster testing self.mb.timeout = 0.4 # overwrite for faster testing
self.mb_start_timeout = 0.5 self.mb_first_timeout = 0.5
self.mb_timeout = 0.5 self.mb_timeout = 0.5
self.writer = Writer() self.writer = Writer()
self.mqtt = Mqtt() self.mqtt = Mqtt()
@@ -632,15 +633,15 @@ def msg_unknown_cmd_rsp(): # 0x1510
@pytest.fixture @pytest.fixture
def config_tsun_allow_all(): def config_tsun_allow_all():
Config.config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}} Config.act_config = {'solarman':{'enabled': True}, 'inverters':{'allow_all':True}}
@pytest.fixture @pytest.fixture
def config_no_tsun_inv1(): def config_no_tsun_inv1():
Config.config = {'solarman':{'enabled': False},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}} Config.act_config = {'solarman':{'enabled': False},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 688}}}
@pytest.fixture @pytest.fixture
def config_tsun_inv1(): def config_tsun_inv1():
Config.config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}} Config.act_config = {'solarman':{'enabled': True},'inverters':{'Y170000000000001':{'monitor_sn': 2070233889, 'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof', 'sensor_list': 688}}}
def test_read_message(device_ind_msg): def test_read_message(device_ind_msg):
m = MemoryStream(device_ind_msg, (0,)) m = MemoryStream(device_ind_msg, (0,))
@@ -772,7 +773,7 @@ def test_invalid_checksum(invalid_checksum, device_ind_msg):
m.close() m.close()
def test_read_message_twice(config_no_tsun_inv1, device_ind_msg, device_rsp_msg): def test_read_message_twice(config_no_tsun_inv1, device_ind_msg, device_rsp_msg):
config_no_tsun_inv1 _ = config_no_tsun_inv1
m = MemoryStream(device_ind_msg, (0,)) m = MemoryStream(device_ind_msg, (0,))
m.append_msg(device_ind_msg) m.append_msg(device_ind_msg)
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -814,7 +815,7 @@ def test_read_message_in_chunks(device_ind_msg):
m.close() m.close()
def test_read_message_in_chunks2(config_tsun_inv1, device_ind_msg): def test_read_message_in_chunks2(config_tsun_inv1, device_ind_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(device_ind_msg, (4,10,0)) m = MemoryStream(device_ind_msg, (4,10,0))
m.read() # read 4 bytes, header incomplere m.read() # read 4 bytes, header incomplere
assert not m.header_valid assert not m.header_valid
@@ -839,10 +840,10 @@ def test_read_message_in_chunks2(config_tsun_inv1, device_ind_msg):
m.close() m.close()
def test_read_two_messages(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg): def test_read_two_messages(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(device_ind_msg, (0,)) m = MemoryStream(device_ind_msg, (0,))
m.append_msg(inverter_ind_msg) m.append_msg(inverter_ind_msg)
assert 0 == m.sensor_list
m._init_new_client_conn() m._init_new_client_conn()
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0 assert m.db.stat['proxy']['Invalid_Msg_Format'] == 0
@@ -857,6 +858,8 @@ def test_read_two_messages(config_tsun_allow_all, device_ind_msg, device_rsp_msg
assert m.msg_recvd[1]['control']==0x4210 assert m.msg_recvd[1]['control']==0x4210
assert m.msg_recvd[1]['seq']=='02:02' assert m.msg_recvd[1]['seq']=='02:02'
assert m.msg_recvd[1]['data_len']==0x199 assert m.msg_recvd[1]['data_len']==0x199
assert '02b0' == m.db.get_db_value(Register.SENSOR_LIST, None)
assert 0x02b0 == m.sensor_list
assert m._forward_buffer==device_ind_msg+inverter_ind_msg assert m._forward_buffer==device_ind_msg+inverter_ind_msg
assert m._send_buffer==device_rsp_msg+inverter_rsp_msg assert m._send_buffer==device_rsp_msg+inverter_rsp_msg
@@ -866,7 +869,7 @@ def test_read_two_messages(config_tsun_allow_all, device_ind_msg, device_rsp_msg
m.close() m.close()
def test_read_two_messages2(config_tsun_allow_all, inverter_ind_msg, inverter_ind_msg_81, inverter_rsp_msg, inverter_rsp_msg_81): def test_read_two_messages2(config_tsun_allow_all, inverter_ind_msg, inverter_ind_msg_81, inverter_rsp_msg, inverter_rsp_msg_81):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(inverter_ind_msg, (0,)) m = MemoryStream(inverter_ind_msg, (0,))
m.append_msg(inverter_ind_msg_81) m.append_msg(inverter_ind_msg_81)
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -892,7 +895,7 @@ def test_read_two_messages2(config_tsun_allow_all, inverter_ind_msg, inverter_in
m.close() m.close()
def test_unkown_message(config_tsun_inv1, unknown_msg): def test_unkown_message(config_tsun_inv1, unknown_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(unknown_msg, (0,)) m = MemoryStream(unknown_msg, (0,))
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -910,7 +913,7 @@ def test_unkown_message(config_tsun_inv1, unknown_msg):
m.close() m.close()
def test_device_rsp(config_tsun_inv1, device_rsp_msg): def test_device_rsp(config_tsun_inv1, device_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(device_rsp_msg, (0,), False) m = MemoryStream(device_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -928,7 +931,7 @@ def test_device_rsp(config_tsun_inv1, device_rsp_msg):
m.close() m.close()
def test_inverter_rsp(config_tsun_inv1, inverter_rsp_msg): def test_inverter_rsp(config_tsun_inv1, inverter_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(inverter_rsp_msg, (0,), False) m = MemoryStream(inverter_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -946,7 +949,7 @@ def test_inverter_rsp(config_tsun_inv1, inverter_rsp_msg):
m.close() m.close()
def test_heartbeat_ind(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg): def test_heartbeat_ind(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(heartbeat_ind_msg, (0,)) m = MemoryStream(heartbeat_ind_msg, (0,))
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -963,7 +966,7 @@ def test_heartbeat_ind(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg):
m.close() m.close()
def test_heartbeat_ind2(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg): def test_heartbeat_ind2(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(heartbeat_ind_msg, (0,)) m = MemoryStream(heartbeat_ind_msg, (0,))
m.no_forwarding = True m.no_forwarding = True
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -981,7 +984,7 @@ def test_heartbeat_ind2(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg):
m.close() m.close()
def test_heartbeat_rsp(config_tsun_inv1, heartbeat_rsp_msg): def test_heartbeat_rsp(config_tsun_inv1, heartbeat_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(heartbeat_rsp_msg, (0,), False) m = MemoryStream(heartbeat_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -999,7 +1002,7 @@ def test_heartbeat_rsp(config_tsun_inv1, heartbeat_rsp_msg):
m.close() m.close()
def test_sync_start_ind(config_tsun_inv1, sync_start_ind_msg, sync_start_rsp_msg, sync_start_fwd_msg): def test_sync_start_ind(config_tsun_inv1, sync_start_ind_msg, sync_start_rsp_msg, sync_start_fwd_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(sync_start_ind_msg, (0,)) m = MemoryStream(sync_start_ind_msg, (0,))
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -1022,7 +1025,7 @@ def test_sync_start_ind(config_tsun_inv1, sync_start_ind_msg, sync_start_rsp_msg
m.close() m.close()
def test_sync_start_rsp(config_tsun_inv1, sync_start_rsp_msg): def test_sync_start_rsp(config_tsun_inv1, sync_start_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(sync_start_rsp_msg, (0,), False) m = MemoryStream(sync_start_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -1040,7 +1043,7 @@ def test_sync_start_rsp(config_tsun_inv1, sync_start_rsp_msg):
m.close() m.close()
def test_sync_end_ind(config_tsun_inv1, sync_end_ind_msg, sync_end_rsp_msg): def test_sync_end_ind(config_tsun_inv1, sync_end_ind_msg, sync_end_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(sync_end_ind_msg, (0,)) m = MemoryStream(sync_end_ind_msg, (0,))
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -1057,7 +1060,7 @@ def test_sync_end_ind(config_tsun_inv1, sync_end_ind_msg, sync_end_rsp_msg):
m.close() m.close()
def test_sync_end_rsp(config_tsun_inv1, sync_end_rsp_msg): def test_sync_end_rsp(config_tsun_inv1, sync_end_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(sync_end_rsp_msg, (0,), False) m = MemoryStream(sync_end_rsp_msg, (0,), False)
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
assert not m.header_valid # must be invalid, since msg was handled and buffer flushed assert not m.header_valid # must be invalid, since msg was handled and buffer flushed
@@ -1075,8 +1078,9 @@ def test_sync_end_rsp(config_tsun_inv1, sync_end_rsp_msg):
m.close() m.close()
def test_build_modell_600(config_tsun_allow_all, inverter_ind_msg): def test_build_modell_600(config_tsun_allow_all, inverter_ind_msg):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(inverter_ind_msg, (0,)) m = MemoryStream(inverter_ind_msg, (0,))
assert 0 == m.sensor_list
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None) assert None == m.db.get_db_value(Register.RATED_POWER, None)
assert None == m.db.get_db_value(Register.INVERTER_TEMP, None) assert None == m.db.get_db_value(Register.INVERTER_TEMP, None)
@@ -1084,6 +1088,8 @@ def test_build_modell_600(config_tsun_allow_all, inverter_ind_msg):
assert 2000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 2000 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert 600 == m.db.get_db_value(Register.RATED_POWER, 0) assert 600 == m.db.get_db_value(Register.RATED_POWER, 0)
assert 'TSOL-MS2000(600)' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0) assert 'TSOL-MS2000(600)' == m.db.get_db_value(Register.EQUIPMENT_MODEL, 0)
assert '02b0' == m.db.get_db_value(Register.SENSOR_LIST, None)
assert 0 == m.sensor_list # must not been set by an inverter data ind
m._send_buffer = bytearray(0) # clear send buffer for next test m._send_buffer = bytearray(0) # clear send buffer for next test
m._init_new_client_conn() m._init_new_client_conn()
@@ -1091,7 +1097,7 @@ def test_build_modell_600(config_tsun_allow_all, inverter_ind_msg):
m.close() m.close()
def test_build_modell_1600(config_tsun_allow_all, inverter_ind_msg1600): def test_build_modell_1600(config_tsun_allow_all, inverter_ind_msg1600):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(inverter_ind_msg1600, (0,)) m = MemoryStream(inverter_ind_msg1600, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None) assert None == m.db.get_db_value(Register.RATED_POWER, None)
@@ -1103,7 +1109,7 @@ def test_build_modell_1600(config_tsun_allow_all, inverter_ind_msg1600):
m.close() m.close()
def test_build_modell_1800(config_tsun_allow_all, inverter_ind_msg1800): def test_build_modell_1800(config_tsun_allow_all, inverter_ind_msg1800):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(inverter_ind_msg1800, (0,)) m = MemoryStream(inverter_ind_msg1800, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None) assert None == m.db.get_db_value(Register.RATED_POWER, None)
@@ -1115,7 +1121,7 @@ def test_build_modell_1800(config_tsun_allow_all, inverter_ind_msg1800):
m.close() m.close()
def test_build_modell_2000(config_tsun_allow_all, inverter_ind_msg2000): def test_build_modell_2000(config_tsun_allow_all, inverter_ind_msg2000):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(inverter_ind_msg2000, (0,)) m = MemoryStream(inverter_ind_msg2000, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None) assert None == m.db.get_db_value(Register.RATED_POWER, None)
@@ -1127,7 +1133,7 @@ def test_build_modell_2000(config_tsun_allow_all, inverter_ind_msg2000):
m.close() m.close()
def test_build_modell_800(config_tsun_allow_all, inverter_ind_msg800): def test_build_modell_800(config_tsun_allow_all, inverter_ind_msg800):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(inverter_ind_msg800, (0,)) m = MemoryStream(inverter_ind_msg800, (0,))
assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0) assert 0 == m.db.get_db_value(Register.MAX_DESIGNED_POWER, 0)
assert None == m.db.get_db_value(Register.RATED_POWER, None) assert None == m.db.get_db_value(Register.RATED_POWER, None)
@@ -1139,7 +1145,7 @@ def test_build_modell_800(config_tsun_allow_all, inverter_ind_msg800):
m.close() m.close()
def test_build_logger_modell(config_tsun_allow_all, device_ind_msg): def test_build_logger_modell(config_tsun_allow_all, device_ind_msg):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(device_ind_msg, (0,)) m = MemoryStream(device_ind_msg, (0,))
assert 0 == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0) assert 0 == m.db.get_db_value(Register.COLLECTOR_FW_VERSION, 0)
assert 'IGEN TECH' == m.db.get_db_value(Register.CHIP_TYPE, None) assert 'IGEN TECH' == m.db.get_db_value(Register.CHIP_TYPE, None)
@@ -1150,6 +1156,7 @@ def test_build_logger_modell(config_tsun_allow_all, device_ind_msg):
m.close() m.close()
def test_msg_iterator(): def test_msg_iterator():
Message._registry.clear()
m1 = SolarmanV5(server_side=True, client_mode=False) m1 = SolarmanV5(server_side=True, client_mode=False)
m2 = SolarmanV5(server_side=True, client_mode=False) m2 = SolarmanV5(server_side=True, client_mode=False)
m3 = SolarmanV5(server_side=True, client_mode=False) m3 = SolarmanV5(server_side=True, client_mode=False)
@@ -1189,7 +1196,7 @@ def test_proxy_counter():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_msg_build_modbus_req(config_tsun_inv1, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, msg_modbus_cmd): async def test_msg_build_modbus_req(config_tsun_inv1, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, msg_modbus_cmd):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(device_ind_msg, (0,), True) m = MemoryStream(device_ind_msg, (0,), True)
m.read() m.read()
assert m.control == 0x4110 assert m.control == 0x4110
@@ -1235,7 +1242,7 @@ async def test_msg_build_modbus_req(config_tsun_inv1, device_ind_msg, device_rsp
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, at_command_ind_msg, at_command_rsp_msg): async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, at_command_ind_msg, at_command_rsp_msg):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(device_ind_msg, (0,), True) m = MemoryStream(device_ind_msg, (0,), True)
m.read() # read device ind m.read() # read device ind
assert m.control == 0x4110 assert m.control == 0x4110
@@ -1292,7 +1299,7 @@ async def test_at_cmd(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inv
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, at_command_ind_msg): async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_msg, inverter_ind_msg, inverter_rsp_msg, at_command_ind_msg):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(device_ind_msg, (0,), True) m = MemoryStream(device_ind_msg, (0,), True)
m.read() m.read()
assert m.control == 0x4110 assert m.control == 0x4110
@@ -1330,7 +1337,7 @@ async def test_at_cmd_blocked(config_tsun_allow_all, device_ind_msg, device_rsp_
m.close() m.close()
def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg): def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(at_command_ind_msg, (0,), False) m = MemoryStream(at_command_ind_msg, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0 m.db.stat['proxy']['AT_Command'] = 0
@@ -1354,7 +1361,7 @@ def test_at_cmd_ind(config_tsun_inv1, at_command_ind_msg):
m.close() m.close()
def test_at_cmd_ind_block(config_tsun_inv1, at_command_ind_msg_block): def test_at_cmd_ind_block(config_tsun_inv1, at_command_ind_msg_block):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(at_command_ind_msg_block, (0,), False) m = MemoryStream(at_command_ind_msg_block, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0 m.db.stat['proxy']['AT_Command'] = 0
@@ -1378,7 +1385,7 @@ def test_at_cmd_ind_block(config_tsun_inv1, at_command_ind_msg_block):
m.close() m.close()
def test_msg_at_command_rsp1(config_tsun_inv1, at_command_rsp_msg): def test_msg_at_command_rsp1(config_tsun_inv1, at_command_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(at_command_rsp_msg) m = MemoryStream(at_command_rsp_msg)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1397,7 +1404,7 @@ def test_msg_at_command_rsp1(config_tsun_inv1, at_command_rsp_msg):
m.close() m.close()
def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg): def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(at_command_rsp_msg) m = MemoryStream(at_command_rsp_msg)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1416,9 +1423,10 @@ def test_msg_at_command_rsp2(config_tsun_inv1, at_command_rsp_msg):
m.close() m.close()
def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd): def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(b'') m = MemoryStream(b'')
m.snr = get_sn_int() m.snr = get_sn_int()
m.sensor_list = 0x2b0
m.state = State.up m.state = State.up
c = m.createClientStream(msg_modbus_cmd) c = m.createClientStream(msg_modbus_cmd)
@@ -1443,7 +1451,7 @@ def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd, msg_modbus_cmd_fwd):
m.close() m.close()
def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd_crc_err): def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd_crc_err):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(b'') m = MemoryStream(b'')
m.snr = get_sn_int() m.snr = get_sn_int()
m.state = State.up m.state = State.up
@@ -1470,7 +1478,7 @@ def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd_crc_err):
m.close() m.close()
def test_msg_unknown_cmd_req(config_tsun_inv1, msg_unknown_cmd): def test_msg_unknown_cmd_req(config_tsun_inv1, msg_unknown_cmd):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_unknown_cmd, (0,), False) m = MemoryStream(msg_unknown_cmd, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['AT_Command'] = 0 m.db.stat['proxy']['AT_Command'] = 0
@@ -1493,7 +1501,7 @@ def test_msg_unknown_cmd_req(config_tsun_inv1, msg_unknown_cmd):
def test_msg_modbus_rsp1(config_tsun_inv1, msg_modbus_rsp): def test_msg_modbus_rsp1(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response without a valid Modbus request must be dropped''' '''Modbus response without a valid Modbus request must be dropped'''
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_rsp) m = MemoryStream(msg_modbus_rsp)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1512,7 +1520,7 @@ def test_msg_modbus_rsp1(config_tsun_inv1, msg_modbus_rsp):
def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp): def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response with a valid Modbus request must be forwarded''' '''Modbus response with a valid Modbus request must be forwarded'''
config_tsun_inv1 # setup config structure _ = config_tsun_inv1 # setup config structure
m = MemoryStream(msg_modbus_rsp) m = MemoryStream(msg_modbus_rsp)
m.mb.rsp_handler = m._SolarmanV5__forward_msg m.mb.rsp_handler = m._SolarmanV5__forward_msg
@@ -1550,7 +1558,7 @@ def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp):
def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp): def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response with a valid Modbus request must be forwarded''' '''Modbus response with a valid Modbus request must be forwarded'''
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_rsp) m = MemoryStream(msg_modbus_rsp)
m.mb.rsp_handler = m._SolarmanV5__forward_msg m.mb.rsp_handler = m._SolarmanV5__forward_msg
@@ -1586,7 +1594,7 @@ def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp):
m.close() m.close()
def test_msg_unknown_rsp(config_tsun_inv1, msg_unknown_cmd_rsp): def test_msg_unknown_rsp(config_tsun_inv1, msg_unknown_cmd_rsp):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_unknown_cmd_rsp) m = MemoryStream(msg_unknown_cmd_rsp)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1604,7 +1612,7 @@ def test_msg_unknown_rsp(config_tsun_inv1, msg_unknown_cmd_rsp):
m.close() m.close()
def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_invalid): def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_invalid):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_invalid, (0,), False) m = MemoryStream(msg_modbus_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1618,7 +1626,7 @@ def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_invalid):
m.close() m.close()
def test_msg_modbus_fragment(config_tsun_inv1, msg_modbus_rsp): def test_msg_modbus_fragment(config_tsun_inv1, msg_modbus_rsp):
config_tsun_inv1 _ = config_tsun_inv1
# receive more bytes than expected (7 bytes from the next msg) # receive more bytes than expected (7 bytes from the next msg)
m = MemoryStream(msg_modbus_rsp+b'\x00\x00\x00\x45\x10\x52\x31', (0,)) m = MemoryStream(msg_modbus_rsp+b'\x00\x00\x00\x45\x10\x52\x31', (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -1644,7 +1652,7 @@ def test_msg_modbus_fragment(config_tsun_inv1, msg_modbus_rsp):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg): async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp_msg):
config_tsun_inv1 _ = config_tsun_inv1
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
m = MemoryStream(heartbeat_ind_msg, (0,)) m = MemoryStream(heartbeat_ind_msg, (0,))
assert asyncio.get_running_loop() == m.mb_timer.loop assert asyncio.get_running_loop() == m.mb_timer.loop
@@ -1665,7 +1673,7 @@ async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp
m._send_buffer = bytearray(0) # clear send buffer for next test m._send_buffer = bytearray(0) # clear send buffer for next test
assert m.state == State.up assert m.state == State.up
assert m.mb_timeout == 0.5 assert isclose(m.mb_timeout, 0.5)
assert next(m.mb_timer.exp_count) == 0 assert next(m.mb_timer.exp_count) == 0
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
@@ -1685,24 +1693,24 @@ async def test_modbus_polling(config_tsun_inv1, heartbeat_ind_msg, heartbeat_rsp
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_start_client_mode(config_tsun_inv1): async def test_start_client_mode(config_tsun_inv1):
config_tsun_inv1 _ = config_tsun_inv1
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
m = MemoryStream(b'') m = MemoryStream(b'')
assert m.state == State.init assert m.state == State.init
assert m.no_forwarding == False assert m.no_forwarding == False
assert m.mb_timer.tim == None assert m.mb_timer.tim == None
assert asyncio.get_running_loop() == m.mb_timer.loop assert asyncio.get_running_loop() == m.mb_timer.loop
await m.send_start_cmd(get_sn_int(), '192.168.1.1', m.mb_start_timeout) await m.send_start_cmd(get_sn_int(), '192.168.1.1', m.mb_first_timeout)
assert m.writer.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x030\x00\x000J\xde\xf1\x15') assert m.writer.sent_pdu==bytearray(b'\xa5\x17\x00\x10E\x01\x00!Ce{\x02\xb0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x030\x00\x000J\xde\xf1\x15')
assert m.db.get_db_value(Register.IP_ADDRESS) == '192.168.1.1' assert m.db.get_db_value(Register.IP_ADDRESS) == '192.168.1.1'
assert m.db.get_db_value(Register.POLLING_INTERVAL) == 0.5 assert isclose(m.db.get_db_value(Register.POLLING_INTERVAL), 0.5)
assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120 assert m.db.get_db_value(Register.HEARTBEAT_INTERVAL) == 120
assert m.state == State.up assert m.state == State.up
assert m.no_forwarding == True assert m.no_forwarding == True
assert m._send_buffer==b'' assert m._send_buffer==b''
assert m.mb_timeout == 0.5 assert isclose(m.mb_timeout, 0.5)
assert next(m.mb_timer.exp_count) == 0 assert next(m.mb_timer.exp_count) == 0
await asyncio.sleep(0.5) await asyncio.sleep(0.5)

View File

@@ -1,5 +1,6 @@
# test_with_pytest.py # test_with_pytest.py
import pytest, logging, asyncio import pytest, logging, asyncio
from math import isclose
from app.src.gen3.talent import Talent, Control from app.src.gen3.talent import Talent, Control
from app.src.config import Config from app.src.config import Config
from app.src.infos import Infos, Register from app.src.infos import Infos, Register
@@ -27,7 +28,7 @@ class MemoryStream(Talent):
super().__init__(server_side) super().__init__(server_side)
if server_side: if server_side:
self.mb.timeout = 0.4 # overwrite for faster testing self.mb.timeout = 0.4 # overwrite for faster testing
self.mb_start_timeout = 0.5 self.mb_first_timeout = 0.5
self.mb_timeout = 0.5 self.mb_timeout = 0.5
self.writer = Writer() self.writer = Writer()
self.__msg = msg self.__msg = msg
@@ -97,12 +98,12 @@ class MemoryStream(Talent):
@pytest.fixture @pytest.fixture
def msg_contact_info(): # Contact Info message def msg_contact_info(): # Contact Info message
Config.config = {'tsun':{'enabled': True}} Config.act_config = {'tsun':{'enabled': True}}
return b'\x00\x00\x00\x2c\x10R170000000000001\x91\x00\x08solarhub\x0fsolarhub\x40123456' return b'\x00\x00\x00\x2c\x10R170000000000001\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture @pytest.fixture
def msg_contact_info_long_id(): # Contact Info message with longer ID def msg_contact_info_long_id(): # Contact Info message with longer ID
Config.config = {'tsun':{'enabled': True}} Config.act_config = {'tsun':{'enabled': True}}
return b'\x00\x00\x00\x2d\x11R1700000000000011\x91\x00\x08solarhub\x0fsolarhub\x40123456' return b'\x00\x00\x00\x2d\x11R1700000000000011\x91\x00\x08solarhub\x0fsolarhub\x40123456'
@pytest.fixture @pytest.fixture
@@ -352,19 +353,19 @@ def msg_unknown(): # Get Time Request message
@pytest.fixture @pytest.fixture
def config_tsun_allow_all(): def config_tsun_allow_all():
Config.config = {'tsun':{'enabled': True}, 'inverters':{'allow_all':True}} Config.act_config = {'tsun':{'enabled': True}, 'inverters':{'allow_all':True}}
@pytest.fixture @pytest.fixture
def config_no_tsun_inv1(): def config_no_tsun_inv1():
Config.config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}} Config.act_config = {'tsun':{'enabled': False},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
@pytest.fixture @pytest.fixture
def config_tsun_inv1(): def config_tsun_inv1():
Config.config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}} Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': True, 'suggested_area':'roof'}}}
@pytest.fixture @pytest.fixture
def config_no_modbus_poll(): def config_no_modbus_poll():
Config.config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': False, 'suggested_area':'roof'}}} Config.act_config = {'tsun':{'enabled': True},'inverters':{'R170000000000001':{'node_id':'inv1', 'modbus_polling': False, 'suggested_area':'roof'}}}
@pytest.fixture @pytest.fixture
def msg_ota_req(): # Over the air update request from tsun cloud def msg_ota_req(): # Over the air update request from tsun cloud
@@ -540,7 +541,7 @@ def test_read_message(msg_contact_info):
m.close() m.close()
def test_read_message_twice(config_no_tsun_inv1, msg_inverter_ind): def test_read_message_twice(config_no_tsun_inv1, msg_inverter_ind):
config_no_tsun_inv1 _ = config_no_tsun_inv1
m = MemoryStream(msg_inverter_ind, (0,)) m = MemoryStream(msg_inverter_ind, (0,))
m.append_msg(msg_inverter_ind) m.append_msg(msg_inverter_ind)
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -621,7 +622,7 @@ def test_read_message_in_chunks2(msg_contact_info):
m.close() m.close()
def test_read_two_messages(config_tsun_allow_all, msg2_contact_info,msg_contact_rsp,msg_contact_rsp2): def test_read_two_messages(config_tsun_allow_all, msg2_contact_info,msg_contact_rsp,msg_contact_rsp2):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(msg2_contact_info, (0,)) m = MemoryStream(msg2_contact_info, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -651,7 +652,7 @@ def test_read_two_messages(config_tsun_allow_all, msg2_contact_info,msg_contact_
m.close() m.close()
def test_msg_contact_resp(config_tsun_inv1, msg_contact_rsp): def test_msg_contact_resp(config_tsun_inv1, msg_contact_rsp):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_contact_rsp, (0,), False) m = MemoryStream(msg_contact_rsp, (0,), False)
m.await_conn_resp_cnt = 1 m.await_conn_resp_cnt = 1
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -671,7 +672,7 @@ def test_msg_contact_resp(config_tsun_inv1, msg_contact_rsp):
m.close() m.close()
def test_msg_contact_resp_2(config_tsun_inv1, msg_contact_rsp): def test_msg_contact_resp_2(config_tsun_inv1, msg_contact_rsp):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_contact_rsp, (0,), False) m = MemoryStream(msg_contact_rsp, (0,), False)
m.await_conn_resp_cnt = 0 m.await_conn_resp_cnt = 0
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -691,7 +692,7 @@ def test_msg_contact_resp_2(config_tsun_inv1, msg_contact_rsp):
m.close() m.close()
def test_msg_contact_resp_3(config_tsun_inv1, msg_contact_rsp): def test_msg_contact_resp_3(config_tsun_inv1, msg_contact_rsp):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_contact_rsp, (0,), True) m = MemoryStream(msg_contact_rsp, (0,), True)
m.await_conn_resp_cnt = 0 m.await_conn_resp_cnt = 0
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -711,7 +712,7 @@ def test_msg_contact_resp_3(config_tsun_inv1, msg_contact_rsp):
m.close() m.close()
def test_msg_contact_invalid(config_tsun_inv1, msg_contact_invalid): def test_msg_contact_invalid(config_tsun_inv1, msg_contact_invalid):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_contact_invalid, (0,)) m = MemoryStream(msg_contact_invalid, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -729,7 +730,7 @@ def test_msg_contact_invalid(config_tsun_inv1, msg_contact_invalid):
m.close() m.close()
def test_msg_get_time(config_tsun_inv1, msg_get_time): def test_msg_get_time(config_tsun_inv1, msg_get_time):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_get_time, (0,)) m = MemoryStream(msg_get_time, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -748,7 +749,7 @@ def test_msg_get_time(config_tsun_inv1, msg_get_time):
m.close() m.close()
def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time): def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time):
config_no_tsun_inv1 _ = config_no_tsun_inv1
m = MemoryStream(msg_get_time, (0,)) m = MemoryStream(msg_get_time, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -767,7 +768,7 @@ def test_msg_get_time_autark(config_no_tsun_inv1, msg_get_time):
m.close() m.close()
def test_msg_time_resp(config_tsun_inv1, msg_time_rsp): def test_msg_time_resp(config_tsun_inv1, msg_time_rsp):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_time_rsp, (0,), False) m = MemoryStream(msg_time_rsp, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -786,7 +787,7 @@ def test_msg_time_resp(config_tsun_inv1, msg_time_rsp):
m.close() m.close()
def test_msg_time_resp_autark(config_no_tsun_inv1, msg_time_rsp): def test_msg_time_resp_autark(config_no_tsun_inv1, msg_time_rsp):
config_no_tsun_inv1 _ = config_no_tsun_inv1
m = MemoryStream(msg_time_rsp, (0,), False) m = MemoryStream(msg_time_rsp, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -805,7 +806,7 @@ def test_msg_time_resp_autark(config_no_tsun_inv1, msg_time_rsp):
m.close() m.close()
def test_msg_time_inv_resp(config_tsun_inv1, msg_time_rsp_inv): def test_msg_time_inv_resp(config_tsun_inv1, msg_time_rsp_inv):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_time_rsp_inv, (0,), False) m = MemoryStream(msg_time_rsp_inv, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -824,7 +825,7 @@ def test_msg_time_inv_resp(config_tsun_inv1, msg_time_rsp_inv):
m.close() m.close()
def test_msg_time_invalid(config_tsun_inv1, msg_time_invalid): def test_msg_time_invalid(config_tsun_inv1, msg_time_invalid):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_time_invalid, (0,), False) m = MemoryStream(msg_time_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -843,7 +844,7 @@ def test_msg_time_invalid(config_tsun_inv1, msg_time_invalid):
m.close() m.close()
def test_msg_time_invalid_autark(config_no_tsun_inv1, msg_time_invalid): def test_msg_time_invalid_autark(config_no_tsun_inv1, msg_time_invalid):
config_no_tsun_inv1 _ = config_no_tsun_inv1
m = MemoryStream(msg_time_invalid, (0,), False) m = MemoryStream(msg_time_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -862,7 +863,7 @@ def test_msg_time_invalid_autark(config_no_tsun_inv1, msg_time_invalid):
m.close() m.close()
def test_msg_cntrl_ind(config_tsun_inv1, msg_controller_ind, msg_controller_ind_ts_offs, msg_controller_ack): def test_msg_cntrl_ind(config_tsun_inv1, msg_controller_ind, msg_controller_ind_ts_offs, msg_controller_ack):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_controller_ind, (0,)) m = MemoryStream(msg_controller_ind, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -885,7 +886,7 @@ def test_msg_cntrl_ind(config_tsun_inv1, msg_controller_ind, msg_controller_ind_
m.close() m.close()
def test_msg_cntrl_ack(config_tsun_inv1, msg_controller_ack): def test_msg_cntrl_ack(config_tsun_inv1, msg_controller_ack):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_controller_ack, (0,), False) m = MemoryStream(msg_controller_ack, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -903,7 +904,7 @@ def test_msg_cntrl_ack(config_tsun_inv1, msg_controller_ack):
m.close() m.close()
def test_msg_cntrl_invalid(config_tsun_inv1, msg_controller_invalid): def test_msg_cntrl_invalid(config_tsun_inv1, msg_controller_invalid):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_controller_invalid, (0,)) m = MemoryStream(msg_controller_invalid, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -926,7 +927,7 @@ def test_msg_cntrl_invalid(config_tsun_inv1, msg_controller_invalid):
m.close() m.close()
def test_msg_inv_ind(config_tsun_inv1, msg_inverter_ind, msg_inverter_ind_ts_offs, msg_inverter_ack): def test_msg_inv_ind(config_tsun_inv1, msg_inverter_ind, msg_inverter_ind_ts_offs, msg_inverter_ack):
config_tsun_inv1 _ = config_tsun_inv1
tracer.setLevel(logging.DEBUG) tracer.setLevel(logging.DEBUG)
m = MemoryStream(msg_inverter_ind, (0,)) m = MemoryStream(msg_inverter_ind, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -950,7 +951,7 @@ def test_msg_inv_ind(config_tsun_inv1, msg_inverter_ind, msg_inverter_ind_ts_off
m.close() m.close()
def test_msg_inv_ind1(config_tsun_inv1, msg_inverter_ind2, msg_inverter_ind_ts_offs, msg_inverter_ack): def test_msg_inv_ind1(config_tsun_inv1, msg_inverter_ind2, msg_inverter_ind_ts_offs, msg_inverter_ack):
config_tsun_inv1 _ = config_tsun_inv1
tracer.setLevel(logging.DEBUG) tracer.setLevel(logging.DEBUG)
m = MemoryStream(msg_inverter_ind2, (0,)) m = MemoryStream(msg_inverter_ind2, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -974,7 +975,7 @@ def test_msg_inv_ind1(config_tsun_inv1, msg_inverter_ind2, msg_inverter_ind_ts_o
m.close() m.close()
def test_msg_inv_ind2(config_tsun_inv1, msg_inverter_ind_new, msg_inverter_ind_ts_offs, msg_inverter_ack): def test_msg_inv_ind2(config_tsun_inv1, msg_inverter_ind_new, msg_inverter_ind_ts_offs, msg_inverter_ack):
config_tsun_inv1 _ = config_tsun_inv1
tracer.setLevel(logging.DEBUG) tracer.setLevel(logging.DEBUG)
m = MemoryStream(msg_inverter_ind_new, (0,)) m = MemoryStream(msg_inverter_ind_new, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -1002,7 +1003,7 @@ def test_msg_inv_ind2(config_tsun_inv1, msg_inverter_ind_new, msg_inverter_ind_t
def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack): def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack):
'''test that after close the invert_status will be resetted if the grid power is <2W''' '''test that after close the invert_status will be resetted if the grid power is <2W'''
config_tsun_inv1 _ = config_tsun_inv1
tracer.setLevel(logging.DEBUG) tracer.setLevel(logging.DEBUG)
m = MemoryStream(msg_inverter_ind_0w, (0,)) m = MemoryStream(msg_inverter_ind_0w, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -1023,13 +1024,13 @@ def test_msg_inv_ind3(config_tsun_inv1, msg_inverter_ind_0w, msg_inverter_ack):
assert m._forward_buffer==msg_inverter_ind_0w assert m._forward_buffer==msg_inverter_ind_0w
assert m._send_buffer==msg_inverter_ack assert m._send_buffer==msg_inverter_ack
assert m.db.get_db_value(Register.INVERTER_STATUS) == None assert m.db.get_db_value(Register.INVERTER_STATUS) == None
assert m.db.db['grid']['Output_Power'] == 0.5 assert isclose(m.db.db['grid']['Output_Power'], 0.5)
m.close() m.close()
assert m.db.get_db_value(Register.INVERTER_STATUS) == 0 assert m.db.get_db_value(Register.INVERTER_STATUS) == 0
def test_msg_inv_ack(config_tsun_inv1, msg_inverter_ack): def test_msg_inv_ack(config_tsun_inv1, msg_inverter_ack):
config_tsun_inv1 _ = config_tsun_inv1
tracer.setLevel(logging.ERROR) tracer.setLevel(logging.ERROR)
m = MemoryStream(msg_inverter_ack, (0,), False) m = MemoryStream(msg_inverter_ack, (0,), False)
@@ -1049,7 +1050,7 @@ def test_msg_inv_ack(config_tsun_inv1, msg_inverter_ack):
m.close() m.close()
def test_msg_inv_invalid(config_tsun_inv1, msg_inverter_invalid): def test_msg_inv_invalid(config_tsun_inv1, msg_inverter_invalid):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_inverter_invalid, (0,), False) m = MemoryStream(msg_inverter_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.read() # read complete msg, and dispatch msg m.read() # read complete msg, and dispatch msg
@@ -1072,7 +1073,7 @@ def test_msg_inv_invalid(config_tsun_inv1, msg_inverter_invalid):
m.close() m.close()
def test_msg_ota_req(config_tsun_inv1, msg_ota_req): def test_msg_ota_req(config_tsun_inv1, msg_ota_req):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_ota_req, (0,), False) m = MemoryStream(msg_ota_req, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['OTA_Start_Msg'] = 0 m.db.stat['proxy']['OTA_Start_Msg'] = 0
@@ -1097,7 +1098,7 @@ def test_msg_ota_req(config_tsun_inv1, msg_ota_req):
m.close() m.close()
def test_msg_ota_ack(config_tsun_inv1, msg_ota_ack): def test_msg_ota_ack(config_tsun_inv1, msg_ota_ack):
config_tsun_inv1 _ = config_tsun_inv1
tracer.setLevel(logging.ERROR) tracer.setLevel(logging.ERROR)
m = MemoryStream(msg_ota_ack, (0,), False) m = MemoryStream(msg_ota_ack, (0,), False)
@@ -1124,7 +1125,7 @@ def test_msg_ota_ack(config_tsun_inv1, msg_ota_ack):
m.close() m.close()
def test_msg_ota_invalid(config_tsun_inv1, msg_ota_invalid): def test_msg_ota_invalid(config_tsun_inv1, msg_ota_invalid):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_ota_invalid, (0,), False) m = MemoryStream(msg_ota_invalid, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['OTA_Start_Msg'] = 0 m.db.stat['proxy']['OTA_Start_Msg'] = 0
@@ -1206,15 +1207,15 @@ def test_timestamp_cnv():
m = MemoryStream(b'') m = MemoryStream(b'')
ts = 1722645998453 # Saturday, 3. August 2024 00:46:38.453 (GMT+2:00) 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 utc =1722638798.453 # GMT: Friday, 2. August 2024 22:46:38.453
assert utc == m._utcfromts(ts) assert isclose(utc, m._utcfromts(ts))
ts = 1691246944000 # Saturday, 5. August 2023 14:49:04 (GMT+2:00) ts = 1691246944000 # Saturday, 5. August 2023 14:49:04 (GMT+2:00)
utc =1691239744.0 # GMT: Saturday, 5. August 2023 12:49:04 utc =1691239744.0 # GMT: Saturday, 5. August 2023 12:49:04
assert utc == m._utcfromts(ts) assert isclose(utc, m._utcfromts(ts))
ts = 1704152544000 # Monday, 1. January 2024 23:42:24 (GMT+1:00) ts = 1704152544000 # Monday, 1. January 2024 23:42:24 (GMT+1:00)
utc =1704148944.0 # GMT: Monday, 1. January 2024 22:42:24 utc =1704148944.0 # GMT: Monday, 1. January 2024 22:42:24
assert utc == m._utcfromts(ts) assert isclose(utc, m._utcfromts(ts))
m.close() m.close()
@@ -1267,7 +1268,7 @@ def test_proxy_counter():
m.close() m.close()
def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd): def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(b'') m = MemoryStream(b'')
m.id_str = b"R170000000000001" m.id_str = b"R170000000000001"
m.state = State.up m.state = State.up
@@ -1298,7 +1299,7 @@ def test_msg_modbus_req(config_tsun_inv1, msg_modbus_cmd):
m.close() m.close()
def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd): def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(b'') m = MemoryStream(b'')
m.id_str = b"R170000000000001" m.id_str = b"R170000000000001"
@@ -1328,7 +1329,7 @@ def test_msg_modbus_req2(config_tsun_inv1, msg_modbus_cmd):
m.close() m.close()
def test_msg_modbus_req3(config_tsun_inv1, msg_modbus_cmd_crc_err): def test_msg_modbus_req3(config_tsun_inv1, msg_modbus_cmd_crc_err):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(b'') m = MemoryStream(b'')
m.id_str = b"R170000000000001" m.id_str = b"R170000000000001"
c = m.createClientStream(msg_modbus_cmd_crc_err) c = m.createClientStream(msg_modbus_cmd_crc_err)
@@ -1357,7 +1358,7 @@ def test_msg_modbus_req3(config_tsun_inv1, msg_modbus_cmd_crc_err):
def test_msg_modbus_rsp1(config_tsun_inv1, msg_modbus_rsp): def test_msg_modbus_rsp1(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response without a valid Modbus request must be dropped''' '''Modbus response without a valid Modbus request must be dropped'''
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_rsp) m = MemoryStream(msg_modbus_rsp)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1378,7 +1379,7 @@ def test_msg_modbus_rsp1(config_tsun_inv1, msg_modbus_rsp):
def test_msg_modbus_cloud_rsp(config_tsun_inv1, msg_modbus_rsp): def test_msg_modbus_cloud_rsp(config_tsun_inv1, msg_modbus_rsp):
'''Modbus response from TSUN without a valid Modbus request must be dropped''' '''Modbus response from TSUN without a valid Modbus request must be dropped'''
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_rsp, (0,), False) m = MemoryStream(msg_modbus_rsp, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Unknown_Msg'] = 0 m.db.stat['proxy']['Unknown_Msg'] = 0
@@ -1401,7 +1402,7 @@ def test_msg_modbus_cloud_rsp(config_tsun_inv1, msg_modbus_rsp):
def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp20): def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp20):
'''Modbus response with a valid Modbus request must be forwarded''' '''Modbus response with a valid Modbus request must be forwarded'''
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_rsp20) m = MemoryStream(msg_modbus_rsp20)
m.append_msg(msg_modbus_rsp20) m.append_msg(msg_modbus_rsp20)
@@ -1431,7 +1432,7 @@ def test_msg_modbus_rsp2(config_tsun_inv1, msg_modbus_rsp20):
def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp21): def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp21):
'''Modbus response with a valid Modbus request must be forwarded''' '''Modbus response with a valid Modbus request must be forwarded'''
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_rsp21) m = MemoryStream(msg_modbus_rsp21)
m.append_msg(msg_modbus_rsp21) m.append_msg(msg_modbus_rsp21)
@@ -1460,7 +1461,7 @@ def test_msg_modbus_rsp3(config_tsun_inv1, msg_modbus_rsp21):
m.close() m.close()
def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_inv): def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_inv):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(msg_modbus_inv, (0,), False) m = MemoryStream(msg_modbus_inv, (0,), False)
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.db.stat['proxy']['Modbus_Command'] = 0 m.db.stat['proxy']['Modbus_Command'] = 0
@@ -1480,7 +1481,7 @@ def test_msg_modbus_invalid(config_tsun_inv1, msg_modbus_inv):
m.close() m.close()
def test_msg_modbus_fragment(config_tsun_inv1, msg_modbus_rsp20): def test_msg_modbus_fragment(config_tsun_inv1, msg_modbus_rsp20):
config_tsun_inv1 _ = config_tsun_inv1
# receive more bytes than expected (7 bytes from the next msg) # receive more bytes than expected (7 bytes from the next msg)
m = MemoryStream(msg_modbus_rsp20+b'\x00\x00\x00\x45\x10\x52\x31', (0,)) m = MemoryStream(msg_modbus_rsp20+b'\x00\x00\x00\x45\x10\x52\x31', (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
@@ -1512,7 +1513,7 @@ def test_msg_modbus_fragment(config_tsun_inv1, msg_modbus_rsp20):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_msg_build_modbus_req(config_tsun_inv1, msg_modbus_cmd): async def test_msg_build_modbus_req(config_tsun_inv1, msg_modbus_cmd):
config_tsun_inv1 _ = config_tsun_inv1
m = MemoryStream(b'', (0,), True) m = MemoryStream(b'', (0,), True)
m.id_str = b"R170000000000001" m.id_str = b"R170000000000001"
await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG) await m.send_modbus_cmd(Modbus.WRITE_SINGLE_REG, 0x2008, 0, logging.DEBUG)
@@ -1538,7 +1539,7 @@ async def test_msg_build_modbus_req(config_tsun_inv1, msg_modbus_cmd):
m.close() m.close()
def test_modbus_no_polling(config_no_modbus_poll, msg_get_time): def test_modbus_no_polling(config_no_modbus_poll, msg_get_time):
config_no_modbus_poll _ = config_no_modbus_poll
m = MemoryStream(msg_get_time, (0,)) m = MemoryStream(msg_get_time, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
m.modbus_polling = False m.modbus_polling = False
@@ -1559,7 +1560,7 @@ def test_modbus_no_polling(config_no_modbus_poll, msg_get_time):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_modbus_polling(config_tsun_inv1, msg_inverter_ind): async def test_modbus_polling(config_tsun_inv1, msg_inverter_ind):
config_tsun_inv1 _ = config_tsun_inv1
assert asyncio.get_running_loop() assert asyncio.get_running_loop()
m = MemoryStream(msg_inverter_ind, (0,)) m = MemoryStream(msg_inverter_ind, (0,))
@@ -1581,7 +1582,7 @@ async def test_modbus_polling(config_tsun_inv1, msg_inverter_ind):
assert m.db.stat['proxy']['Unknown_Ctrl'] == 0 assert m.db.stat['proxy']['Unknown_Ctrl'] == 0
m._send_buffer = bytearray(0) # clear send buffer for next test m._send_buffer = bytearray(0) # clear send buffer for next test
assert m.mb_timeout == 0.5 assert isclose(m.mb_timeout, 0.5)
assert next(m.mb_timer.exp_count) == 0 assert next(m.mb_timer.exp_count) == 0
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
@@ -1599,7 +1600,7 @@ async def test_modbus_polling(config_tsun_inv1, msg_inverter_ind):
m.close() m.close()
def test_broken_recv_buf(config_tsun_allow_all, broken_recv_buf): def test_broken_recv_buf(config_tsun_allow_all, broken_recv_buf):
config_tsun_allow_all _ = config_tsun_allow_all
m = MemoryStream(broken_recv_buf, (0,)) m = MemoryStream(broken_recv_buf, (0,))
m.db.stat['proxy']['Unknown_Ctrl'] = 0 m.db.stat['proxy']['Unknown_Ctrl'] = 0
assert m.db.stat['proxy']['Invalid_Data_Type'] == 0 assert m.db.stat['proxy']['Invalid_Data_Type'] == 0

1
requirements-test.txt Normal file
View File

@@ -0,0 +1 @@
-r ./app/requirements-test.txt

26
sonar-project.properties Normal file
View File

@@ -0,0 +1,26 @@
sonar.projectKey=s-allius_tsun-gen3-proxy
sonar.organization=s-allius
# This is the name and version displayed in the SonarCloud UI.
sonar.projectName=tsun-gen3-proxy
#sonar.projectVersion=1.0
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
sonar.sources=app/src/
# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
sonar.python.version=3.12
sonar.tests=system_tests/,app/tests/
sonar.exclusions=**/.vscode/**/*
# Name your criteria
sonar.issue.ignore.multicriteria=e1,e2
# python:S905 : Remove or refactor this statement; it has no side effects
sonar.issue.ignore.multicriteria.e1.ruleKey=python:S905
sonar.issue.ignore.multicriteria.e1.resourceKey=app/tests/*.py
sonar.issue.ignore.multicriteria.e2.ruleKey=python:S905
sonar.issue.ignore.multicriteria.e2.resourceKey=systems_tests/*.py