diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index c7e3a30..dc86fd7 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -9,13 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: master - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.10 - name: Install setuptools run: | diff --git a/.github/workflows/run_tox.yml b/.github/workflows/run_tox.yml index b1dfb09..752fb73 100644 --- a/.github/workflows/run_tox.yml +++ b/.github/workflows/run_tox.yml @@ -8,12 +8,12 @@ jobs: strategy: max-parallel: 2 matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 515bb5d..974064c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,18 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.4.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/pycqa/isort - rev: 5.8.0 + rev: v5.11.3 hooks: - id: isort name: isort (python) - - repo: https://gitlab.com/PyCQA/flake8 - rev: '3.9.1' + - repo: https://github.com/PyCQA/flake8 + rev: '6.0.0' hooks: - id: flake8 diff --git a/requirements_setup.txt b/requirements_setup.txt index 8f1b4f7..d0c6c6e 100644 --- a/requirements_setup.txt +++ b/requirements_setup.txt @@ -1,5 +1,5 @@ -asyncio-mqtt == 0.12.1 +asyncio-mqtt == 0.16.1 pyserial-asyncio == 0.6 -easyconfig == 0.2.4 +easyconfig == 0.2.6 pydantic >= 1.10, <2.0 smllib == 1.2 diff --git a/src/sml2mqtt/__main__.py b/src/sml2mqtt/__main__.py index 149a8a3..aa9ed42 100644 --- a/src/sml2mqtt/__main__.py +++ b/src/sml2mqtt/__main__.py @@ -20,7 +20,7 @@ async def a_main(): try: for port_cfg in CONFIG.ports: dev_mqtt = BASE_TOPIC.create_child(port_cfg.url) - await Device.create(port_cfg.url, port_cfg.timeout, set(), dev_mqtt) + await Device.create(port_cfg, port_cfg.timeout, set(), dev_mqtt) except Exception as e: shutdown(e) diff --git a/src/sml2mqtt/__version__.py b/src/sml2mqtt/__version__.py index 3f262a6..923b987 100644 --- a/src/sml2mqtt/__version__.py +++ b/src/sml2mqtt/__version__.py @@ -1 +1 @@ -__version__ = '1.2.1' +__version__ = '1.2.2' diff --git a/src/sml2mqtt/config/config.py b/src/sml2mqtt/config/config.py index 2403252..bd74b72 100644 --- a/src/sml2mqtt/config/config.py +++ b/src/sml2mqtt/config/config.py @@ -1,7 +1,8 @@ from typing import Dict, List, Union +import serial from easyconfig import AppBaseModel, BaseModel, create_app_config -from pydantic import constr, Field +from pydantic import constr, Field, StrictFloat, StrictInt, validator from .device import REPUBLISH_ALIAS, SmlDeviceConfig, SmlValueConfig from .logging import LoggingSettings @@ -13,6 +14,42 @@ class PortSettings(BaseModel): timeout: Union[int, float] = Field( default=3, description='Seconds after which a timeout will be detected (default=3)') + baudrate: int = Field(9600, in_file=False) + parity: str = Field('None', in_file=False) + stopbits: Union[StrictInt, StrictFloat] = Field(serial.STOPBITS_ONE, in_file=False, alias='stop bits') + bytesize: int = Field(serial.EIGHTBITS, in_file=False, alias='byte size') + + @validator('baudrate') + def _val_baudrate(cls, v): + if v not in serial.Serial.BAUDRATES: + raise ValueError(f'must be one of {list(serial.Serial.BAUDRATES)}') + return v + + @validator('parity') + def _val_parity(cls, v): + # Short name + if v in serial.PARITY_NAMES: + return v + + # Name -> Short name + parity_values = {_n: _v for _v, _n in serial.PARITY_NAMES.items()} + if v not in parity_values: + raise ValueError(f'must be one of {list(parity_values)}') + return parity_values[v] + + @validator('stopbits') + def _val_stopbits(cls, v): + if v not in serial.Serial.STOPBITS: + raise ValueError(f'must be one of {list(serial.Serial.STOPBITS)}') + return v + + @validator('bytesize') + def _val_bytesize(cls, v): + if v not in serial.Serial.BYTESIZES: + raise ValueError(f'must be one of {list(serial.Serial.BYTESIZES)}') + return v + + class GeneralSettings(BaseModel): wh_in_kwh: bool = Field(True, description='Automatically convert Wh to kWh', alias='Wh in kWh') @@ -65,4 +102,4 @@ def default_config() -> Settings: return s -CONFIG = create_app_config(Settings(), default_config) +CONFIG: Settings = create_app_config(Settings(), default_config) diff --git a/src/sml2mqtt/device/sml_device.py b/src/sml2mqtt/device/sml_device.py index d2d9188..3ae502e 100644 --- a/src/sml2mqtt/device/sml_device.py +++ b/src/sml2mqtt/device/sml_device.py @@ -11,6 +11,7 @@ from sml2mqtt.__log__ import get_logger from sml2mqtt.__shutdown__ import shutdown from sml2mqtt.config import CONFIG +from sml2mqtt.config.config import PortSettings from sml2mqtt.config.device import SmlDeviceConfig from sml2mqtt.device import DeviceStatus from sml2mqtt.device.watchdog import Watchdog @@ -23,11 +24,11 @@ class Device: @classmethod - async def create(cls, url: str, timeout: float, skip_values: Set[str], mqtt_device: MqttObj): + async def create(cls, settings: PortSettings, timeout: float, skip_values: Set[str], mqtt_device: MqttObj): try: - device = cls(url, timeout, set(skip_values), mqtt_device) - device.serial = await sml2mqtt.device.SmlSerial.create(url, device) - ALL_DEVICES[url] = device + device = cls(settings.url, timeout, set(skip_values), mqtt_device) + device.serial = await sml2mqtt.device.SmlSerial.create(settings, device) + ALL_DEVICES[settings.url] = device return device except Exception as e: raise DeviceSetupFailed(e) from None diff --git a/src/sml2mqtt/device/sml_serial.py b/src/sml2mqtt/device/sml_serial.py index a9c8ada..0833779 100644 --- a/src/sml2mqtt/device/sml_serial.py +++ b/src/sml2mqtt/device/sml_serial.py @@ -5,6 +5,7 @@ from serial_asyncio import create_serial_connection, SerialTransport from sml2mqtt.__log__ import get_logger +from sml2mqtt.config.config import PortSettings from sml2mqtt.device import DeviceStatus if TYPE_CHECKING: @@ -16,11 +17,14 @@ class SmlSerial(asyncio.Protocol): @classmethod - async def create(cls, url: str, device: 'sml2mqtt.device_id.Device') -> 'SmlSerial': + async def create(cls, settings: PortSettings, device: 'sml2mqtt.device_id.Device') -> 'SmlSerial': transport, protocol = await create_serial_connection( - asyncio.get_event_loop(), cls, url, baudrate=9600) # type: SerialTransport, SmlSerial + asyncio.get_event_loop(), cls, + url=settings.url, + baudrate=settings.baudrate, parity=settings.parity, + stopbits=settings.stopbits, bytesize=settings.bytesize) # type: SerialTransport, SmlSerial - protocol.url = url + protocol.url = settings.url protocol.device = device return protocol diff --git a/tests/config/test_default.py b/tests/config/test_default.py index 7e57ac9..caa747d 100644 --- a/tests/config/test_default.py +++ b/tests/config/test_default.py @@ -27,29 +27,29 @@ def test_default(): Wh in kWh: true # Automatically convert Wh to kWh republish after: 120 # Republish automatically after this time (if no other filter configured) ports: -- url: COM1 - timeout: 3 -- url: /dev/ttyS0 - timeout: 3 +- url: COM1 # Device path + timeout: 3 # Seconds after which a timeout will be detected (default=3) +- url: /dev/ttyS0 # Device path + timeout: 3 # Seconds after which a timeout will be detected (default=3) devices: # Device configuration by ID or url DEVICE_ID_HEX: mqtt: topic: DEVICE_BASE_TOPIC status: topic: status - skip: + skip: # OBIS codes (HEX) of values that will not be published (optional) - OBIS - values: + values: # Special configurations for each of the values (optional) OBIS: - mqtt: + mqtt: # Mqtt config for this entry (optional) topic: OBIS - workarounds: + workarounds: # Workarounds for the value (optional) - negative on energy meter status: true - transformations: + transformations: # Mathematical transformations for the value (optional) - factor: 3 - offset: 100 - round: 2 - filters: + filters: # Refresh options for the value (optional) - diff: 10 - perc: 10 - every: 120 diff --git a/tests/device/conftest.py b/tests/device/conftest.py index f0d273d..37e42b0 100644 --- a/tests/device/conftest.py +++ b/tests/device/conftest.py @@ -4,6 +4,7 @@ import sml2mqtt.device.sml_device import sml2mqtt.device.sml_serial +from sml2mqtt.config.config import PortSettings from sml2mqtt.device import Device, DeviceStatus from sml2mqtt.mqtt import MqttObj @@ -30,7 +31,7 @@ async def device(no_serial): mqtt_base = MqttObj('testing', 0, False).update() mqtt_device = mqtt_base.create_child(device_url) - obj = await Device.create(device_url, 1, set(), mqtt_device) + obj = await Device.create(PortSettings(url=device_url), 1, set(), mqtt_device) # Wrapper so we see the traceback in the tests def wrapper(func):