diff --git a/.github/workflows/run_tox.yml b/.github/workflows/run_tox.yml index 1c3d54c..1d01b5e 100644 --- a/.github/workflows/run_tox.yml +++ b/.github/workflows/run_tox.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.10' - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 tests: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3549780..ac21cd7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-ast - id: check-builtin-literals @@ -14,8 +14,13 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.5 + rev: v0.7.2 hooks: + - id: ruff + name: ruff unused imports + # F401 [*] {name} imported but unused + args: [ "--select", "F401", "--extend-exclude", "__init__.py", "--fix"] + - id: ruff # I001 [*] Import block is un-sorted or un-formatted # UP035 [*] Import from {target} instead: {names} @@ -23,7 +28,6 @@ repos: # Q001 [*] Double quote multiline found but single quotes preferred args: [ "--select", "I001,UP035,Q000,Q001", "--fix"] - - id: ruff - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 @@ -31,6 +35,14 @@ repos: - id: rst-backticks + - repo: https://github.com/JelleZijlstra/autotyping + rev: 24.9.0 + hooks: + - id: autotyping + types: [python] + args: [--safe] + + - repo: meta hooks: - id: check-hooks-apply diff --git a/.ruff.toml b/.ruff.toml index 02e66b1..4378640 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,61 +1,74 @@ - -line-length = 120 indent-width = 4 +line-length = 120 target-version = "py310" -src = ["src", "test"] - -[lint] -select = [ - "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w - "I", # https://docs.astral.sh/ruff/rules/#isort-i - "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up - - "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a - "ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async - "C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 - "EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em - "FIX", # https://docs.astral.sh/ruff/rules/#flake8-fixme-fix - "INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp - "ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc - "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - "PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth - "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret - "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim - "SLOT", # https://docs.astral.sh/ruff/rules/#flake8-slots-slot - "T10", # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 - "TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch - "TD", # https://docs.astral.sh/ruff/rules/#flake8-todos-td - - "TRY", # https://docs.astral.sh/ruff/rules/#tryceratops-try - "FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly - "PERF", # https://docs.astral.sh/ruff/rules/#perflint-perf - "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf - - # "PL", # https://docs.astral.sh/ruff/rules/#pylint-pl - # "FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb +src = [ + "src", + "tests" ] + +[lint] +select = ["ALL"] + ignore = [ + "D", # https://docs.astral.sh/ruff/rules/#pydocstyle-d + "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "SLF", # https://docs.astral.sh/ruff/rules/#flake8-self-slf + "RET501", # https://docs.astral.sh/ruff/rules/unnecessary-return-none/#unnecessary-return-none-ret501 "TRY400", # https://docs.astral.sh/ruff/rules/error-instead-of-exception/ - "A003", # https://docs.astral.sh/ruff/rules/builtin-attribute-shadowing/ + # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "A003", # Python builtin is shadowed by class attribute {name} from {row} + + # https://docs.astral.sh/ruff/rules/#pyflakes-f + "F401", # {name} imported but unused; consider using importlib.util.find_spec to test for availability + + # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + + # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "UP038", # Use X | Y in {} call instead of (X, Y) + + # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + "ANN101", # Missing type annotation for {name} in method + "ANN102", # Missing type annotation for {name} in classmethod + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} + + # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble + "BLE001", # Do not catch blind exception: {name} + + # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + "RSE102", # Unnecessary parentheses on raised exception + + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "COM812", # Trailing comma missing + "COM819", # Trailing comma prohibited - "UP038", # https://docs.astral.sh/ruff/rules/non-pep604-isinstance/ + # https://docs.astral.sh/ruff/rules/#warning-w_1 + "PLW0603", # Using the global statement to update {name} is discouraged + + # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "G004", # Logging statement uses f-string + + # https://docs.astral.sh/ruff/rules/#refactor-r + "PLR1711", # Useless return statement at end of function + + # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "RUF005", # Consider {expression} instead of concatenation ] [format] -# Use single quotes for non-triple-quoted strings. quote-style = "single" # https://docs.astral.sh/ruff/settings/#lintflake8-quotes [lint.flake8-quotes] -inline-quotes = "single" +inline-quotes = "single" multiline-quotes = "single" @@ -63,17 +76,28 @@ multiline-quotes = "single" builtins-ignorelist = ["id", "input"] +# https://docs.astral.sh/ruff/settings/#lintisort +[lint.isort] +lines-after-imports = 2 # https://docs.astral.sh/ruff/settings/#lint_isort_lines-after-imports + + [lint.per-file-ignores] -"docs/conf.py" = ["INP001", "A001"] -"setup.py" = ["PTH123"] -"tests/*" = [ - "ISC002", # Implicitly concatenated string literals over multiple lines - "E501", # Line too long - "INP001", # File `FILE_NAME` is part of an implicit namespace package. Add an `__init__.py` +"docs/conf.py" = [ + "INP001", # File `conf.py` is part of an implicit namespace package. Add an `__init__.py`. + "A001", # Variable `copyright` is shadowing a Python builtin + "PTH118", # `os.path.join()` should be replaced by `Path` with `/` operator + "PTH100", # `os.path.abspath()` should be replaced by `Path.resolve()` ] +"setup.py" = ["PTH123"] + +"tests/*" = [ + "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "S101", # Use of assert detected -[lint.isort] -# https://docs.astral.sh/ruff/settings/#lint_isort_lines-after-imports -lines-after-imports = 2 + # https://docs.astral.sh/ruff/rules/#refactor-r + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLR0913", # Too many arguments in function definition ({c_args} > {max_args}) +] diff --git a/readme.md b/readme.md index 856351e..d61d41f 100644 --- a/readme.md +++ b/readme.md @@ -21,6 +21,9 @@ To read from the serial port an IR to USB reader for energy meter is required. # Changelog +#### 3.2 (2024-11-05) +- Automatically select CRC e.g. for Holley DTZ541 + #### 3.1 (2024-08-05) - Updated dependencies - Added some small log messages diff --git a/requirements.txt b/requirements.txt index 245c0e8..2ed8888 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -r requirements_setup.txt # Testing -pytest == 8.3.2 -pre-commit == 3.8.0 -pytest-asyncio == 0.23.8 +pytest == 8.3.3 +pre-commit == 4.0.1 +pytest-asyncio == 0.24.0 aioresponses == 0.7.6 # Linter -ruff == 0.5.6 +ruff == 0.7.2 diff --git a/requirements_setup.txt b/requirements_setup.txt index 46b9c82..9c474bb 100644 --- a/requirements_setup.txt +++ b/requirements_setup.txt @@ -1,6 +1,6 @@ -aiomqtt == 2.2.0 +aiomqtt == 2.3.0 pyserial-asyncio == 0.6 easyconfig == 0.3.2 pydantic == 2.8.2 -smllib == 1.4 -aiohttp == 3.10.1 +smllib == 1.5 +aiohttp == 3.10.10 diff --git a/src/sml2mqtt/__log__.py b/src/sml2mqtt/__log__.py index 92b2fb5..15f35c6 100644 --- a/src/sml2mqtt/__log__.py +++ b/src/sml2mqtt/__log__.py @@ -18,7 +18,7 @@ def get_logger(suffix: str) -> logging.Logger: class MidnightRotatingFileHandler(RotatingFileHandler): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.last_check: date = datetime.now().date() @@ -29,7 +29,7 @@ def shouldRollover(self, record) -> int: return super().shouldRollover(record) -def setup_log(): +def setup_log() -> None: level = sml2mqtt.CONFIG.logging.set_log_level() # This is the longest logger name str diff --git a/src/sml2mqtt/__main__.py b/src/sml2mqtt/__main__.py index a21c0c0..7bdf16e 100644 --- a/src/sml2mqtt/__main__.py +++ b/src/sml2mqtt/__main__.py @@ -13,7 +13,7 @@ from sml2mqtt.sml_source import create_source -async def a_main(): +async def a_main() -> None: # Add possibility to stop program with Ctrl + c signal_handler_setup() diff --git a/src/sml2mqtt/__version__.py b/src/sml2mqtt/__version__.py index 27d4bb6..d7e6e82 100644 --- a/src/sml2mqtt/__version__.py +++ b/src/sml2mqtt/__version__.py @@ -1 +1 @@ -__version__ = '3.1' +__version__ = '3.2' diff --git a/src/sml2mqtt/config/config.py b/src/sml2mqtt/config/config.py index 44f508e..144588c 100644 --- a/src/sml2mqtt/config/config.py +++ b/src/sml2mqtt/config/config.py @@ -31,13 +31,18 @@ class GeneralSettings(BaseModel): description='Additional OBIS fields for the serial number to configuration matching', alias='device id obis', in_file=False ) + crc: list[str] = Field( + default=['x25', 'kermit'], + description='Which crc algorithms are used to calculate the checksum of the smart meter', + alias='crc', in_file=False + ) class Settings(AppBaseModel): logging: LoggingSettings = Field(default_factory=LoggingSettings) mqtt: MqttConfig = Field(default_factory=MqttConfig) general: GeneralSettings = Field(default_factory=GeneralSettings) - inputs: list[HttpSourceSettings | SerialSourceSettings] = Field([], discriminator='type') + inputs: list[HttpSourceSettings | SerialSourceSettings] = Field(default_factory=list, discriminator='type') devices: dict[LowerStr, SmlDeviceConfig] = Field({}, description='Device configuration by ID or url',) diff --git a/src/sml2mqtt/config/logging.py b/src/sml2mqtt/config/logging.py index da3e308..01dc9eb 100644 --- a/src/sml2mqtt/config/logging.py +++ b/src/sml2mqtt/config/logging.py @@ -1,7 +1,7 @@ import logging from easyconfig import BaseModel -from pydantic import Extra, Field, field_validator +from pydantic import Field, field_validator class LoggingSettings(BaseModel): diff --git a/src/sml2mqtt/config/operations.py b/src/sml2mqtt/config/operations.py index 6c706bd..915f759 100644 --- a/src/sml2mqtt/config/operations.py +++ b/src/sml2mqtt/config/operations.py @@ -11,7 +11,7 @@ from sml2mqtt.const import DateTimeFinder, DurationType, TimeSeries -from .types import Number, ObisHex, PercentStr # noqa: TCH001 +from .types import Number, ObisHex # noqa: TCH001 class EmptyKwargs(TypedDict): diff --git a/src/sml2mqtt/const/protocols.py b/src/sml2mqtt/const/protocols.py index a8053f9..3076eeb 100644 --- a/src/sml2mqtt/const/protocols.py +++ b/src/sml2mqtt/const/protocols.py @@ -8,19 +8,19 @@ class DeviceProto(Protocol): def name(self) -> str: ... - def on_source_data(self, data: bytes): + def on_source_data(self, data: bytes) -> None: ... - def on_source_failed(self, reason: str): + def on_source_failed(self, reason: str) -> None: ... - def on_error(self, e: Exception, *, show_traceback: bool = True): + def on_error(self, e: Exception, *, show_traceback: bool = True) -> None: ... class SourceProto(Protocol): - def start(self): + def start(self) -> None: ... - def cancel_and_wait(self): + def cancel_and_wait(self) -> None: ... diff --git a/src/sml2mqtt/const/sml_helpers.py b/src/sml2mqtt/const/sml_helpers.py index 7c80086..fb8f88c 100644 --- a/src/sml2mqtt/const/sml_helpers.py +++ b/src/sml2mqtt/const/sml_helpers.py @@ -57,14 +57,14 @@ def create(cls, timestamp: float, values: Iterable[SmlListEntry]): c.values[value.obis] = value return c - def __init__(self, timestamp: float): + def __init__(self, timestamp: float) -> None: self.timestamp: Final = timestamp self.values: Final[dict[str, SmlListEntry]] = {} def __getattr__(self, item: str) -> SmlListEntry: return self.values[item] - def __len__(self): + def __len__(self) -> int: return len(self.values) def get_value(self, obis: str) -> SmlListEntry | None: diff --git a/src/sml2mqtt/const/task.py b/src/sml2mqtt/const/task.py index f38548f..66de7e6 100644 --- a/src/sml2mqtt/const/task.py +++ b/src/sml2mqtt/const/task.py @@ -27,7 +27,7 @@ def create_task(coro: Coroutine, *, name: str | None = None): return task -async def wait_for_tasks(): +async def wait_for_tasks() -> None: while True: for task in TASKS.copy(): @@ -49,7 +49,7 @@ async def wait_for_tasks(): class Task: - def __init__(self, coro: Callable[[], Awaitable], *, name: str): + def __init__(self, coro: Callable[[], Awaitable], *, name: str) -> None: self._coro: Final = coro self._name: Final = name @@ -61,7 +61,7 @@ def is_running(self) -> bool: return False return True - def start(self): + def start(self) -> None: if not self.is_running: self._task = create_task(self._wrapper(), name=self._name) @@ -82,7 +82,7 @@ async def cancel_and_wait(self) -> bool: pass return True - async def _wrapper(self): + async def _wrapper(self) -> None: task = current_task() try: @@ -97,17 +97,17 @@ async def _wrapper(self): log.debug(f'{self._name:s} finished!') - def process_exception(self, e: Exception): + def process_exception(self, e: Exception) -> None: log.error(f'Error in {self._name:s}') for line in traceback.format_exc().splitlines(): log.error(line) class DeviceTask(Task): - def __init__(self, device: DeviceProto, coro: Callable[[], Coroutine], *, name: str): + def __init__(self, device: DeviceProto, coro: Callable[[], Coroutine], *, name: str) -> None: super().__init__(coro, name=name) self._device: Final = device - def process_exception(self, e: Exception): + def process_exception(self, e: Exception) -> None: super().process_exception(e) self._device.on_source_failed(f'Task crashed: {e.__class__.__name__}') diff --git a/src/sml2mqtt/const/time_series.py b/src/sml2mqtt/const/time_series.py index 0ff599a..f9a74fc 100644 --- a/src/sml2mqtt/const/time_series.py +++ b/src/sml2mqtt/const/time_series.py @@ -18,7 +18,7 @@ def get_duration(obj: DurationType) -> int | float: class TimeSeries: __slots__ = ('period', 'times', 'values', 'is_full', 'wait_for_data') - def __init__(self, period: DurationType, wait_for_data: bool = False): + def __init__(self, period: DurationType, wait_for_data: bool = False) -> None: self.wait_for_data: Final = wait_for_data self.period: Final = get_duration(period) self.times: Final[deque[float]] = deque() @@ -26,12 +26,12 @@ def __init__(self, period: DurationType, wait_for_data: bool = False): self.is_full: bool = False - def clear(self): + def clear(self) -> None: self.is_full = False self.times.clear() self.values.clear() - def add_value(self, value: int | float | None, timestamp: float): + def add_value(self, value: int | float | None, timestamp: float) -> None: start = timestamp - self.period if value is not None: diff --git a/src/sml2mqtt/errors.py b/src/sml2mqtt/errors.py index 71ab5dc..1854aae 100644 --- a/src/sml2mqtt/errors.py +++ b/src/sml2mqtt/errors.py @@ -57,15 +57,15 @@ def log_msg(self, log: Logger): # ------------------------------------------------------------------------------------ class HttpStatusError(Sml2MqttExceptionWithLog): - def __init__(self, status: int): + def __init__(self, status: int) -> None: super().__init__() self.status: Final = status - def __str__(self): + def __str__(self) -> str: return f'{self.__class__.__name__:s}: {self.status:d}' @override - def log_msg(self, log: Logger): + def log_msg(self, log: Logger) -> None: log.error(f'Received http status {self.status}') def __eq__(self, other): @@ -78,12 +78,12 @@ def __eq__(self, other): # Value Processing Errors # ------------------------------------------------------------------------------------ class UnprocessedObisValuesReceivedError(Sml2MqttExceptionWithLog): - def __init__(self, *values: SmlListEntry): + def __init__(self, *values: SmlListEntry) -> None: super().__init__() self.values: Final = values @override - def log_msg(self, log: Logger): + def log_msg(self, log: Logger) -> None: log.error(f'Unexpected obis id{"" if len(self.values) == 1 else "s"} received!') for value in self.values: for line in value.format_msg().splitlines(): @@ -91,10 +91,10 @@ def log_msg(self, log: Logger): class RequiredObisValueNotInFrameError(Sml2MqttExceptionWithLog): - def __init__(self, *obis: str): + def __init__(self, *obis: str) -> None: super().__init__() self.obis: Final = obis @override - def log_msg(self, log: Logger): + def log_msg(self, log: Logger) -> None: log.error(f'Expected obis id{"" if len(self.obis) == 1 else "s"} missing in frame: {", ".join(self.obis)}!') diff --git a/src/sml2mqtt/mqtt/connect_delay.py b/src/sml2mqtt/mqtt/connect_delay.py index 272bd3b..202438c 100644 --- a/src/sml2mqtt/mqtt/connect_delay.py +++ b/src/sml2mqtt/mqtt/connect_delay.py @@ -1,8 +1,9 @@ from asyncio import sleep +from types import TracebackType class DynDelay: - def __init__(self, min_delay: float, max_delay: float, start_delay: float | None = None): + def __init__(self, min_delay: float, max_delay: float, start_delay: float | None = None) -> None: if min_delay < 0: msg = f'min_delay must be >= 0: {min_delay}' raise ValueError(msg) @@ -18,21 +19,21 @@ def __init__(self, min_delay: float, max_delay: float, start_delay: float | None self.curr = max(min(start_delay, max_delay), min_delay) - async def wait(self): + async def wait(self) -> None: await sleep(self.curr) - def increase(self): + def increase(self) -> None: self.curr = min(self.max, self.curr * 2) if not self.curr: self.curr = 1 - def reset(self): + def reset(self) -> None: self.curr = self.min - async def __aenter__(self): + async def __aenter__(self) -> None: await self.wait() - async def __aexit__(self, exc_type, exc_val, exc_tb): + async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> bool: # noqa: E501 if exc_type is None: self.reset() else: diff --git a/src/sml2mqtt/mqtt/mqtt.py b/src/sml2mqtt/mqtt/mqtt.py index 0d161ab..972bf17 100644 --- a/src/sml2mqtt/mqtt/mqtt.py +++ b/src/sml2mqtt/mqtt/mqtt.py @@ -19,7 +19,7 @@ IS_CONNECTED: Event | None = None -async def start(): +async def start() -> None: global IS_CONNECTED, TASK assert TASK is None @@ -126,6 +126,6 @@ async def _mqtt_task() -> None: IS_CONNECTED.clear() -def publish(topic: str, value: int | float | str | bytes, qos: int, retain: bool): +def publish(topic: str, value: int | float | str | bytes, qos: int, retain: bool) -> None: if QUEUE is not None: QUEUE.put_nowait((topic, value, qos, retain)) diff --git a/src/sml2mqtt/mqtt/mqtt_obj.py b/src/sml2mqtt/mqtt/mqtt_obj.py index cd0a08e..ef4202c 100644 --- a/src/sml2mqtt/mqtt/mqtt_obj.py +++ b/src/sml2mqtt/mqtt/mqtt_obj.py @@ -12,11 +12,11 @@ pub_func: Callable[[str, int | float | str, int, bool], Any] = publish -def publish_analyze(topic: str, value: int | float | str, qos: int, retain: bool): +def publish_analyze(topic: str, value: int | float | str, qos: int, retain: bool) -> None: get_logger('mqtt.pub').info(f'{topic}: {value} (QOS: {qos}, retain: {retain})') -def patch_analyze(): +def patch_analyze() -> None: global pub_func pub_func = publish_analyze @@ -29,7 +29,7 @@ class MqttCfg: qos: int | None = None retain: bool | None = None - def set_config(self, config: OptionalMqttPublishConfig): + def set_config(self, config: OptionalMqttPublishConfig) -> None: self.topic_full = config.full_topic self.topic_fragment = config.topic self.qos = config.qos @@ -37,7 +37,7 @@ def set_config(self, config: OptionalMqttPublishConfig): class MqttObj: - def __init__(self, topic_fragment: str | None = None, qos: int | None = None, retain: bool | None = None): + def __init__(self, topic_fragment: str | None = None, qos: int | None = None, retain: bool | None = None) -> None: # Configured parts self.cfg = MqttCfg(topic_fragment=topic_fragment, qos=qos, retain=retain) @@ -50,7 +50,7 @@ def __init__(self, topic_fragment: str | None = None, qos: int | None = None, re self.parent: MqttObj | None = None self.children: list[MqttObj] = [] - def publish(self, value: str | int | float): + def publish(self, value: str | int | float) -> None: pub_func(self.topic, value, self.qos, self.retain) def update(self) -> 'MqttObj': @@ -130,14 +130,14 @@ def iter_objs(self) -> Generator['MqttObj', Any, None]: BASE_TOPIC: Final = MqttObj() -def setup_base_topic(topic: str, qos: int, retain: bool): +def setup_base_topic(topic: str, qos: int, retain: bool) -> None: BASE_TOPIC.cfg.topic_fragment = topic BASE_TOPIC.cfg.qos = qos BASE_TOPIC.cfg.retain = retain BASE_TOPIC.update() -def check_for_duplicate_topics(obj: MqttObj): +def check_for_duplicate_topics(obj: MqttObj) -> None: log = get_logger('mqtt') topics: set[str] = set() diff --git a/src/sml2mqtt/runtime/shutdown.py b/src/sml2mqtt/runtime/shutdown.py index 4f330a6..b9c430b 100644 --- a/src/sml2mqtt/runtime/shutdown.py +++ b/src/sml2mqtt/runtime/shutdown.py @@ -22,7 +22,7 @@ class ShutdownObj: coro: Callable[[], Awaitable] msg: str - async def do(self): + async def do(self) -> None: try: log.debug(self.msg) await self.coro() @@ -34,7 +34,7 @@ async def do(self): log.error(line) -async def shutdown_coro(): +async def shutdown_coro() -> None: log.debug('Starting shutdown') for obj in SHUTDOWN_OBJS: await obj.do() @@ -56,7 +56,7 @@ def on_shutdown(coro: Callable[[], Awaitable], msg: str): SHUTDOWN_CALL: Task | None = None -async def do_shutdown_async(): +async def do_shutdown_async() -> None: global SHUTDOWN_CALL try: @@ -78,10 +78,10 @@ def do_shutdown(): SHUTDOWN_CALL = create_task(do_shutdown_async()) -def _signal_handler_shutdown(sig, frame): +def _signal_handler_shutdown(sig, frame) -> None: do_shutdown() -def signal_handler_setup(): +def signal_handler_setup() -> None: signal.signal(signal.SIGINT, _signal_handler_shutdown) signal.signal(signal.SIGTERM, _signal_handler_shutdown) diff --git a/src/sml2mqtt/sml_device/setup_device.py b/src/sml2mqtt/sml_device/setup_device.py index ade8f66..d792978 100644 --- a/src/sml2mqtt/sml_device/setup_device.py +++ b/src/sml2mqtt/sml_device/setup_device.py @@ -40,7 +40,7 @@ def has_operation_type(obj: OperationContainerBase, *ops: type[ValueOperationBas def _create_default_transformations(log: logging.Logger, sml_value: SmlValue, frame: SmlFrameValues, - general_cfg: GeneralSettings): + general_cfg: GeneralSettings) -> None: op_count = len(sml_value.operations) @@ -71,7 +71,7 @@ def _create_default_filters(log: logging.Logger, sml_value: SmlValue, general_cf sml_value.add_operation(RefreshActionOperation(general_cfg.republish_after)) -def setup_device(device: SmlDevice, frame: SmlFrameValues, cfg: SmlDeviceConfig | None, general_cfg: GeneralSettings): +def setup_device(device: SmlDevice, frame: SmlFrameValues, cfg: SmlDeviceConfig | None, general_cfg: GeneralSettings) -> None: mqtt_device = device.mqtt_device skip_default_setup = set() diff --git a/src/sml2mqtt/sml_device/sml_device.py b/src/sml2mqtt/sml_device/sml_device.py index e19bcf6..073d15c 100644 --- a/src/sml2mqtt/sml_device/sml_device.py +++ b/src/sml2mqtt/sml_device/sml_device.py @@ -8,7 +8,9 @@ import smllib from smllib import SmlStreamReader from smllib.errors import CrcError +from typing_extensions import Self +from sml2mqtt import CONFIG from sml2mqtt.__log__ import get_logger from sml2mqtt.const import EnhancedSmlFrame from sml2mqtt.errors import ObisIdForConfigurationMappingNotFoundError, Sml2MqttExceptionWithLog @@ -16,9 +18,9 @@ from sml2mqtt.sml_device.sml_devices import ALL_DEVICES from sml2mqtt.sml_value import SmlValues -from .. import CONFIG from .device_status import DeviceStatus from .setup_device import setup_device +from .stream_reader_group import StreamReaderGroup, create_stream_reader_group from .watchdog import Watchdog @@ -35,13 +37,13 @@ class SmlDevice: - def __init__(self, name: str): + def __init__(self, name: str) -> None: self._name: Final = name self._source: SourceProto | None = None self.status: DeviceStatus = DeviceStatus.STARTUP self.watchdog: Final = Watchdog(self) - self.stream_reader: Final = SmlStreamReader() + self.stream_reader: StreamReaderGroup | SmlStreamReader = create_stream_reader_group() self.log = get_logger(self.name) self.log_status = self.log.getChild('status') @@ -58,17 +60,17 @@ def __init__(self, name: str): def name(self) -> str: return self._name - def set_source(self, source: SourceProto): + def set_source(self, source: SourceProto) -> Self: assert self._source is None, self._source self._source = source return self - async def start(self): + async def start(self) -> None: if self._source is not None: self._source.start() self.watchdog.start() - async def cancel_and_wait(self): + async def cancel_and_wait(self) -> None: if self._source is not None: await self._source.cancel_and_wait() await self.watchdog.cancel_and_wait() @@ -79,7 +81,7 @@ def set_status(self, new_status: DeviceStatus) -> bool: self.status = new_status - # Don't log toggeling between CRC_ERROR and OK. Only log if new status is not OK + # Don't log toggling between CRC_ERROR and OK. Only log if new status is not OK level = LVL_INFO if new_status is DeviceStatus.CRC_ERROR: level = LVL_DEBUG @@ -96,7 +98,7 @@ def set_status(self, new_status: DeviceStatus) -> bool: ALL_DEVICES.check_status() return True - def on_source_data(self, data: bytes): + def on_source_data(self, data: bytes) -> None: frame = None # type: EnhancedSmlFrame | None try: @@ -121,7 +123,7 @@ def on_source_data(self, data: bytes): self.on_error(e) - def on_error(self, e: Exception, *, show_traceback: bool = True): + def on_error(self, e: Exception, *, show_traceback: bool = True) -> None: self.log.debug(f'Exception {type(e)}: "{e}"') # Log exception @@ -138,14 +140,14 @@ def on_error(self, e: Exception, *, show_traceback: bool = True): self.set_status(DeviceStatus.ERROR) return None - def on_source_failed(self, reason: str): + def on_source_failed(self, reason: str) -> None: self.log.error(f'Source failed: {reason}') self.set_status(DeviceStatus.SOURCE_FAILED) - def on_timeout(self): + def on_timeout(self) -> None: self.set_status(DeviceStatus.MSG_TIMEOUT) - def process_frame(self, frame: EnhancedSmlFrame): + def process_frame(self, frame: EnhancedSmlFrame) -> None: frame_values = frame.get_frame_values(self.log) @@ -154,7 +156,13 @@ def process_frame(self, frame: EnhancedSmlFrame): # There was no Error -> OK self.set_status(DeviceStatus.OK) - def setup_values_from_frame(self, frame: EnhancedSmlFrame): + def setup_values_from_frame(self, frame: EnhancedSmlFrame) -> None: + if not isinstance(self.stream_reader, SmlStreamReader): + self.stream_reader = self.stream_reader.get_reader() + _func_name = self.stream_reader.crc_func.__name__ + crc_func_name = getattr(self.stream_reader.crc_func, '__module__', _func_name).rsplit('.', 1)[-1] + self.log.debug(f'Using crc {crc_func_name:s}') + frame_values = frame.get_frame_values(self.log) # search frame and see if we get a match @@ -178,16 +186,19 @@ def setup_values_from_frame(self, frame: EnhancedSmlFrame): self.frame_handler = self.process_frame - def process_first_frame(self, frame: EnhancedSmlFrame): + def process_first_frame(self, frame: EnhancedSmlFrame) -> None: + try: self.setup_values_from_frame(frame) except Exception as e: self.set_status(DeviceStatus.SHUTDOWN) self.on_error(e) return None + self.frame_handler(frame) + return None - def analyze_frame(self, frame: EnhancedSmlFrame): + def analyze_frame(self, frame: EnhancedSmlFrame) -> None: # log Frame and frame description for line in frame.get_analyze_str(): diff --git a/src/sml2mqtt/sml_device/sml_devices.py b/src/sml2mqtt/sml_device/sml_devices.py index 4ea1a1d..4f9e877 100644 --- a/src/sml2mqtt/sml_device/sml_devices.py +++ b/src/sml2mqtt/sml_device/sml_devices.py @@ -23,11 +23,11 @@ def add_device(self, device: SmlDevice) -> SmlDevice: self._devices = (*self._devices, device) return device - async def start(self): + async def start(self) -> None: for device in self._devices: await device.start() - async def cancel_and_wait(self): + async def cancel_and_wait(self) -> None: for device in self._devices: await device.cancel_and_wait() diff --git a/src/sml2mqtt/sml_device/stream_reader_group.py b/src/sml2mqtt/sml_device/stream_reader_group.py new file mode 100644 index 0000000..19cd170 --- /dev/null +++ b/src/sml2mqtt/sml_device/stream_reader_group.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from smllib import SmlFrame, SmlStreamReader +from smllib.errors import CrcError + +from sml2mqtt import CONFIG + + +class StreamReaderGroup: + def __init__(self, *crcs: str) -> None: + self.readers = [SmlStreamReader(crc=crc) for crc in crcs] + self.last_reader: SmlStreamReader | None = None + + def add(self, _bytes: bytes) -> None: + for reader in self.readers: + reader.add(_bytes) + + def get_frame(self) -> SmlFrame | None: + crc_errors = [] + last_frame: SmlFrame | None = None + for reader in self.readers: + try: + if (ret := reader.get_frame()) is None: + continue + + if last_frame is not None: + msg = 'Multiple frames' + raise ValueError(msg) + + if self.last_reader is not None and self.last_reader is not reader: + msg = 'Reader changed' + raise ValueError(msg) + + self.last_reader = reader + last_frame = ret + except CrcError as e: + crc_errors.append(e) + + if last_frame is not None: + return last_frame + + # all readers issued a crc error -> propagate + if len(crc_errors) == len(self.readers): + raise crc_errors[0] from None + + return None + + def get_reader(self) -> SmlStreamReader: + if self.last_reader is None: + msg = 'Last reader not set' + raise ValueError(msg) + return self.last_reader + + +def create_stream_reader_group() -> StreamReaderGroup: + crc_algorithms = CONFIG.general.crc + return StreamReaderGroup(*crc_algorithms) diff --git a/src/sml2mqtt/sml_device/watchdog.py b/src/sml2mqtt/sml_device/watchdog.py index 62fb067..69649bb 100644 --- a/src/sml2mqtt/sml_device/watchdog.py +++ b/src/sml2mqtt/sml_device/watchdog.py @@ -11,17 +11,17 @@ class Watchdog: - def __init__(self, device: SmlDevice): + def __init__(self, device: SmlDevice) -> None: self._timeout: float = -1 self.device: Final = device self._event: Final = Event() self._task: Final = DeviceTask(device, self._wd_task, name=f'Watchdog Task {self.device.name:s}') - def start(self): + def start(self) -> None: self._task.start() - def cancel(self): + def cancel(self) -> None: self._task.cancel() async def cancel_and_wait(self): @@ -33,10 +33,10 @@ def set_timeout(self, timeout: float): self._timeout = timeout return self - def feed(self): + def feed(self) -> None: self._event.set() - async def _wd_task(self): + async def _wd_task(self) -> None: make_call = True while True: self._event.clear() diff --git a/src/sml2mqtt/sml_source/serial.py b/src/sml2mqtt/sml_source/serial.py index 5b9fb01..becfbf2 100644 --- a/src/sml2mqtt/sml_source/serial.py +++ b/src/sml2mqtt/sml_source/serial.py @@ -45,13 +45,13 @@ def __init__(self, device: DeviceProto, url: str) -> None: self.last_read: float | None = 0.0 - def start(self): + def start(self) -> None: self._task.start() async def cancel_and_wait(self): return await self._task.cancel_and_wait() - def connection_made(self, transport: SerialTransport): + def connection_made(self, transport: SerialTransport) -> None: self.transport = transport log.debug(f'Port {self.url:s} successfully opened') @@ -69,13 +69,13 @@ def connection_lost(self, exc: Exception | None) -> None: log.log(lvl, f'Port {self.url:s} was closed{ex_str:s}') self.device.on_source_failed(f'Connection to port {self.url:s} lost') - def data_received(self, data: bytes): + def data_received(self, data: bytes) -> None: self.transport.pause_reading() self.last_read = monotonic() self.device.on_source_data(data) - async def _chunk_task(self): + async def _chunk_task(self) -> None: interval = 0.2 while True: diff --git a/src/sml2mqtt/sml_value/base.py b/src/sml2mqtt/sml_value/base.py index f2672e1..8e6befe 100644 --- a/src/sml2mqtt/sml_value/base.py +++ b/src/sml2mqtt/sml_value/base.py @@ -14,13 +14,13 @@ class SmlValueInfo: __slots__ = ('value', 'frame', 'last_pub') - def __init__(self, sml: SmlListEntry, frame: SmlFrameValues, last_pub: float): + def __init__(self, sml: SmlListEntry, frame: SmlFrameValues, last_pub: float) -> None: self.value: Final = sml self.frame: Final = frame self.last_pub: Final = last_pub - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__} obis={self.value.obis}>' diff --git a/src/sml2mqtt/sml_value/operations/actions.py b/src/sml2mqtt/sml_value/operations/actions.py index 5122716..8519986 100644 --- a/src/sml2mqtt/sml_value/operations/actions.py +++ b/src/sml2mqtt/sml_value/operations/actions.py @@ -10,7 +10,7 @@ class RefreshActionOperation(ValueOperationBase): - def __init__(self, every: DurationType): + def __init__(self, every: DurationType) -> None: self.every: Final = get_duration(every) self.last_time: float = -1 self.last_value: float | None = None @@ -28,7 +28,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None self.last_time = monotonic() return self.last_value - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -37,7 +37,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class HeartbeatActionOperation(ValueOperationBase): - def __init__(self, every: DurationType): + def __init__(self, every: DurationType) -> None: self.every: Final = get_duration(every) self.last_time: float = -1_000_000_000 self.last_value: float | None = None @@ -53,7 +53,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None self.last_time = monotonic() return self.last_value - def __repr__(self): + def __repr__(self) -> str: return f'' @override diff --git a/src/sml2mqtt/sml_value/operations/date_time.py b/src/sml2mqtt/sml_value/operations/date_time.py index 0e1313c..818a3d4 100644 --- a/src/sml2mqtt/sml_value/operations/date_time.py +++ b/src/sml2mqtt/sml_value/operations/date_time.py @@ -15,7 +15,7 @@ class SupportsDateTimeAction(ValueOperationWithStartupBase): - def __init__(self, dt_finder: DateTimeFinder, start_now: bool = True): + def __init__(self, dt_finder: DateTimeFinder, start_now: bool = True) -> None: self._dt_finder: Final = dt_finder self._next_reset: datetime = dt_finder.get_first_reset(start_now) @@ -52,7 +52,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class VirtualMeterOperation(SupportsDateTimeAction): - def __init__(self, dt_finder: DateTimeFinder, start_now: bool): + def __init__(self, dt_finder: DateTimeFinder, start_now: bool) -> None: super().__init__(dt_finder, start_now) self.last_value: float | None = None self.offset: float | None = None @@ -78,7 +78,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None return value - offset - def __repr__(self): + def __repr__(self) -> str: return (f'') @@ -90,7 +90,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class MaxValueOperation(SupportsDateTimeAction): - def __init__(self, dt_finder: DateTimeFinder, start_now: bool): + def __init__(self, dt_finder: DateTimeFinder, start_now: bool) -> None: super().__init__(dt_finder, start_now) self.max_value: float | None = None @@ -122,7 +122,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class MinValueOperation(SupportsDateTimeAction): - def __init__(self, dt_finder: DateTimeFinder, start_now: bool): + def __init__(self, dt_finder: DateTimeFinder, start_now: bool) -> None: super().__init__(dt_finder, start_now) self.min_value: float | None = None diff --git a/src/sml2mqtt/sml_value/operations/filter.py b/src/sml2mqtt/sml_value/operations/filter.py index dce38c1..e82b2b9 100644 --- a/src/sml2mqtt/sml_value/operations/filter.py +++ b/src/sml2mqtt/sml_value/operations/filter.py @@ -24,7 +24,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None self.last_value = value return value - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -34,7 +34,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class RangeFilterOperation(ValueOperationBase): # noinspection PyShadowingBuiltins - def __init__(self, min_value: float | None, max_value: float | None, limit_values: bool = True): + def __init__(self, min_value: float | None, max_value: float | None, limit_values: bool = True) -> None: self.min_value: Final = min_value self.max_value: Final = max_value self.limit_values: Final = limit_values @@ -52,7 +52,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None return value - def __repr__(self): + def __repr__(self) -> str: return (f'') @@ -67,7 +67,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class DeltaFilterOperation(ValueOperationBase): - def __init__(self, min_value: int | float | None = None, min_percent: int | float | None = None): + def __init__(self, min_value: int | float | None = None, min_percent: int | float | None = None) -> None: self.min_value: Final = min_value self.min_percent: Final = min_percent @@ -95,7 +95,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None self.last_value = value return value - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -115,7 +115,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None return None return value - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -124,7 +124,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class ThrottleFilterOperation(ValueOperationBase): - def __init__(self, period: DurationType): + def __init__(self, period: DurationType) -> None: self.period: Final = get_duration(period) self.last_time: float = -1_000_000_000 @@ -140,7 +140,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None self.last_time = now return value - def __repr__(self): + def __repr__(self) -> str: return f'' @override diff --git a/src/sml2mqtt/sml_value/operations/math.py b/src/sml2mqtt/sml_value/operations/math.py index d2e53f7..bca186d 100644 --- a/src/sml2mqtt/sml_value/operations/math.py +++ b/src/sml2mqtt/sml_value/operations/math.py @@ -7,7 +7,7 @@ class FactorOperation(ValueOperationBase): - def __init__(self, factor: int | float): + def __init__(self, factor: int | float) -> None: self.factor: Final = factor @override @@ -16,7 +16,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None return None return value * self.factor - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -25,7 +25,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class OffsetOperation(ValueOperationBase): - def __init__(self, offset: int | float): + def __init__(self, offset: int | float) -> None: self.offset: Final = offset @override @@ -34,7 +34,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None return None return value + self.offset - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -43,7 +43,7 @@ def describe(self, indent: str = '') -> Generator[str, None, None]: class RoundOperation(ValueOperationBase): - def __init__(self, digits: int): + def __init__(self, digits: int) -> None: self.digits: Final = digits if digits else None @override @@ -55,7 +55,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None return value return round(value, self.digits) - def __repr__(self): + def __repr__(self) -> str: return f'' @override diff --git a/src/sml2mqtt/sml_value/operations/operations.py b/src/sml2mqtt/sml_value/operations/operations.py index 09ffacd..83079b8 100644 --- a/src/sml2mqtt/sml_value/operations/operations.py +++ b/src/sml2mqtt/sml_value/operations/operations.py @@ -16,7 +16,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None return ret - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -33,7 +33,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None value = op.process_value(value, info) return value - def __repr__(self): + def __repr__(self) -> str: return f'' @override diff --git a/src/sml2mqtt/sml_value/operations/time_series.py b/src/sml2mqtt/sml_value/operations/time_series.py index 4709d29..af61753 100644 --- a/src/sml2mqtt/sml_value/operations/time_series.py +++ b/src/sml2mqtt/sml_value/operations/time_series.py @@ -9,7 +9,7 @@ class TimeSeriesOperationBaseBase(ValueOperationBase): - def __init__(self, time_series: TimeSeries, reset_after_value: bool): + def __init__(self, time_series: TimeSeries, reset_after_value: bool) -> None: self.time_series: Final = time_series self.reset_after_value: Final = reset_after_value @@ -66,7 +66,7 @@ class MaxOfIntervalOperation(TimeSeriesOperationBase): def on_values(self, obj: Sequence[float]) -> float | None: return max(obj) - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -81,7 +81,7 @@ class MinOfIntervalOperation(TimeSeriesOperationBase): def on_values(self, obj: Sequence[float]) -> float | None: return min(obj) - def __repr__(self): + def __repr__(self) -> str: return f'' @override @@ -104,7 +104,7 @@ def on_values(self, obj: Sequence[tuple[float, float]]) -> float | None: return None return mean / time - def __repr__(self): + def __repr__(self) -> str: return f'' @override diff --git a/src/sml2mqtt/sml_value/operations/workarounds.py b/src/sml2mqtt/sml_value/operations/workarounds.py index a2996ad..91cc7c3 100644 --- a/src/sml2mqtt/sml_value/operations/workarounds.py +++ b/src/sml2mqtt/sml_value/operations/workarounds.py @@ -8,7 +8,7 @@ class NegativeOnEnergyMeterWorkaroundOperation(ValueOperationBase): - def __init__(self, meter_obis: str | None = None): + def __init__(self, meter_obis: str | None = None) -> None: self.meter_obis: Final[str] = '0100010800ff' if meter_obis is None else meter_obis @override @@ -30,7 +30,7 @@ def process_value(self, value: float | None, info: SmlValueInfo) -> float | None return value - def __repr__(self): + def __repr__(self) -> str: return f'' @override diff --git a/src/sml2mqtt/sml_value/setup_operations.py b/src/sml2mqtt/sml_value/setup_operations.py index 1e98051..f0b476c 100644 --- a/src/sml2mqtt/sml_value/setup_operations.py +++ b/src/sml2mqtt/sml_value/setup_operations.py @@ -107,7 +107,7 @@ class _HasOperationsProto(Protocol): operations: list[BaseModel] -def setup_operations(parent: OperationContainerBase, cfg_parent: _HasOperationsProto): +def setup_operations(parent: OperationContainerBase, cfg_parent: _HasOperationsProto) -> None: for cfg in cfg_parent.operations: factory = get_operation_factory(cfg) diff --git a/src/sml2mqtt/sml_value/sml_value.py b/src/sml2mqtt/sml_value/sml_value.py index 86e7c4d..b999bb6 100644 --- a/src/sml2mqtt/sml_value/sml_value.py +++ b/src/sml2mqtt/sml_value/sml_value.py @@ -8,7 +8,7 @@ class SmlValue(OperationContainerBase): - def __init__(self, obis: str, mqtt: MqttObj): + def __init__(self, obis: str, mqtt: MqttObj) -> None: super().__init__() self.obis: Final = obis @@ -16,7 +16,7 @@ def __init__(self, obis: str, mqtt: MqttObj): self.last_publish: float = 0 - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__} obis={self.obis} at 0x{id(self):x}>' def process_frame(self, frame: SmlFrameValues): diff --git a/src/sml2mqtt/sml_value/sml_values.py b/src/sml2mqtt/sml_value/sml_values.py index fbf2676..406dc24 100644 --- a/src/sml2mqtt/sml_value/sml_values.py +++ b/src/sml2mqtt/sml_value/sml_values.py @@ -19,7 +19,7 @@ def __init__(self) -> None: self._all_ids: frozenset[str] = frozenset() self._values: tuple[SmlValue, ...] = () - def __repr__(self): + def __repr__(self) -> str: return ( f'<{self.__class__.__name__:s} ' f'processed={",".join(self._processed_ids):s}, ' diff --git a/tests/config/test_config.py b/tests/config/test_config.py index fca1809..87c8ae3 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -5,7 +5,7 @@ from sml2mqtt.config.config import SmlValueConfig -def test_err_msg(): +def test_err_msg() -> None: with pytest.raises(ValidationError) as e: SmlValueConfig.model_validate({ @@ -20,7 +20,7 @@ def test_err_msg(): " Invalid key names [type=invalid_key_names, input_value={'factor': 1, 'offset': 2}, input_type=dict]" -def test_error_message(): +def test_error_message() -> None: with pytest.raises(ValidationError) as e: SmlValueConfig.model_validate({ diff --git a/tests/config/test_default.py b/tests/config/test_default.py index f41f933..1a39152 100644 --- a/tests/config/test_default.py +++ b/tests/config/test_default.py @@ -3,7 +3,7 @@ from sml2mqtt.config import CONFIG -def test_default(): +def test_default() -> None: yaml = CONFIG.generate_default_yaml() # Replace dynamically created identifier diff --git a/tests/config/test_types.py b/tests/config/test_types.py index bd66e71..d657889 100644 --- a/tests/config/test_types.py +++ b/tests/config/test_types.py @@ -8,7 +8,7 @@ from sml2mqtt.const import DurationType # noqa: TCH001 -def test_obis(): +def test_obis() -> None: class TestObis(BaseModel): value: ObisHex @@ -17,7 +17,7 @@ class TestObis(BaseModel): assert TestObis.model_validate({'value': '0100000009FF '}).value == '0100000009ff' -def test_duration(): +def test_duration() -> None: class TestObis(BaseModel): value: DurationType diff --git a/tests/conftest.py b/tests/conftest.py index bec6de8..96b0246 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING import pytest -from helper import PatchedSmlStreamReader from tests.sml_data import ( sml_data_1, sml_data_1_analyze, @@ -17,9 +16,10 @@ import sml2mqtt.const.task as task_module import sml2mqtt.mqtt.mqtt_obj +from helper import PatchedSmlStreamReader from sml2mqtt import CMD_ARGS from sml2mqtt.runtime import shutdown as shutdown_module -from sml2mqtt.sml_device import sml_device as sml_device_module +from sml2mqtt.sml_device import stream_reader_group as reader_group_module if TYPE_CHECKING: @@ -36,30 +36,30 @@ class PatchedMonotonic: - def __init__(self): + def __init__(self) -> None: self._now: int | float = 0 self._mp = pytest.MonkeyPatch() def _get_monotonic(self): return self._now - def patch_name(self, target: str): + def patch_name(self, target: str) -> None: self._mp.setattr(target, self._get_monotonic) - def patch(self, target: str | object, name: str | object): + def patch(self, target: str | object, name: str | object) -> None: self._mp.setattr(target, name, value=self._get_monotonic) - def undo(self): + def undo(self) -> None: self._mp.undo() - def add(self, secs: float): + def add(self, secs: float) -> None: self._now += secs - def set(self, secs: float): + def set(self, secs: float) -> None: self._now = secs -@pytest.fixture() +@pytest.fixture def monotonic(): p = PatchedMonotonic() @@ -72,27 +72,31 @@ def monotonic(): p.undo() -@pytest.fixture() +@pytest.fixture def no_mqtt(monkeypatch): pub_list = [] - def pub_func(topic: str, value, qos: int, retain: bool): + def pub_func(topic: str, value, qos: int, retain: bool) -> None: pub_list.append((topic, value, qos, retain)) monkeypatch.setattr(sml2mqtt.mqtt.mqtt_obj, 'pub_func', pub_func) return pub_list -@pytest.fixture() +@pytest.fixture def stream_reader(monkeypatch): r = PatchedSmlStreamReader() - monkeypatch.setattr(sml_device_module, 'SmlStreamReader', lambda: r) + + def factory(crc: str): + return r + + monkeypatch.setattr(reader_group_module, 'SmlStreamReader', factory) return r @pytest.fixture(autouse=True) -def _clear_mqtt(monkeypatch): +def _clear_mqtt(monkeypatch) -> None: monkeypatch.setattr(sml2mqtt.mqtt.BASE_TOPIC, 'children', []) @@ -128,7 +132,7 @@ def check_no_logged_error(caplog, request): @pytest.fixture(autouse=True) -def _wrap_all_tasks(monkeypatch): +def _wrap_all_tasks(monkeypatch) -> None: async def wrapped_future(coro): try: @@ -146,7 +150,7 @@ def create_task(coro, *, name=None): monkeypatch.setattr(task_module, 'create_task', create_task) -@pytest.fixture() +@pytest.fixture def arg_analyze(monkeypatch): monkeypatch.setattr(CMD_ARGS, 'analyze', True) sml2mqtt.mqtt.patch_analyze() diff --git a/tests/const/test_protocols.py b/tests/const/test_protocols.py index 8c87931..4fcc3e4 100644 --- a/tests/const/test_protocols.py +++ b/tests/const/test_protocols.py @@ -7,7 +7,7 @@ from sml2mqtt.sml_device import SmlDevice -def assert_signatures(a, b): +def assert_signatures(a, b) -> None: sig_a = inspect.signature(a) if a is not None else a sig_b = inspect.signature(b) if b is not None else b assert sig_a == sig_b, f'\n {sig_a}\n {sig_b}\n' @@ -16,7 +16,7 @@ def assert_signatures(a, b): @pytest.mark.parametrize( ('proto', 'cls'), [(DeviceProto, SmlDevice), ] ) -def test_protocols(proto: type[Protocol], cls: type): +def test_protocols(proto: type[Protocol], cls: type) -> None: for name, proto_obj in inspect.getmembers(proto): if name.startswith('_'): continue diff --git a/tests/const/test_time_series.py b/tests/const/test_time_series.py index 538c4d4..8a3fa20 100644 --- a/tests/const/test_time_series.py +++ b/tests/const/test_time_series.py @@ -1,7 +1,7 @@ from sml2mqtt.const import TimeSeries -def test_time_series_boundaries(): +def test_time_series_boundaries() -> None: for wait_for_data in (True, False): t = TimeSeries(10, wait_for_data=wait_for_data) @@ -22,7 +22,7 @@ def test_time_series_boundaries(): assert t.is_full -def test_time_series(): +def test_time_series() -> None: t = TimeSeries(10) t.add_value(1, 3) @@ -43,7 +43,7 @@ def test_time_series(): assert t.get_value_duration(20) == [(2, 5), (3, 2), (4, 3), (5, 0)] -def test_time_series_start(): +def test_time_series_start() -> None: t = TimeSeries(10, wait_for_data=False) for _ in range(2): @@ -70,7 +70,7 @@ def test_time_series_start(): t.clear() -def test_time_series_start_wait_for_data(): +def test_time_series_start_wait_for_data() -> None: t = TimeSeries(10, wait_for_data=True) for _ in range(2): diff --git a/tests/helper.py b/tests/helper.py index 33bdd29..67ed410 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,5 +1,6 @@ from asyncio import sleep from collections.abc import Callable +from typing import Literal from unittest.mock import Mock from smllib import SmlStreamReader @@ -30,8 +31,8 @@ class PatchedSmlStreamReader(SmlStreamReader): _CRC_ERROR = 'CRC_ERROR' @override - def __init__(self, build_ctx: CTX_HINT | None = None): - super().__init__(build_ctx) + def __init__(self, build_ctx: CTX_HINT | None = None, crc: Literal['kermit', 'x25'] = 'x25') -> None: + super().__init__(build_ctx, crc=crc) self.returns = [] def add(self, _bytes: bytes | EnhancedSmlFrame | str): @@ -48,7 +49,7 @@ def add(self, _bytes: bytes | EnhancedSmlFrame | str): else: raise TypeError() - def clear(self): + def clear(self) -> None: super().clear() def get_frame(self) -> EnhancedSmlFrame | None: diff --git a/tests/sml_data.py b/tests/sml_data.py index 447767c..c4ea8bc 100644 --- a/tests/sml_data.py +++ b/tests/sml_data.py @@ -6,7 +6,7 @@ from sml2mqtt.const import EnhancedSmlFrame, SmlFrameValues -@pytest.fixture() +@pytest.fixture def sml_data_1(): return a2b_hex( b'1B1B1B1B01010101760501188E6162006200726500000101760101070000000000000B000000000000000000000101636877007' @@ -19,7 +19,7 @@ def sml_data_1(): ) -@pytest.fixture() +@pytest.fixture def sml_frame_1(stream_reader): frame = EnhancedSmlFrame(a2b_hex( b'760500531efa620062007263010176010105001bb4fe0b0a0149534b0005020de272620165001bb32e620163a71400760500531e' @@ -33,7 +33,7 @@ def sml_frame_1(stream_reader): return frame -@pytest.fixture() +@pytest.fixture def sml_frame_2(stream_reader): frame = EnhancedSmlFrame(a2b_hex( b'7605065850a66200620072630101760107ffffffffffff05021d70370b0a014c475a0003403b4972620165021d7707016326de' @@ -48,7 +48,7 @@ def sml_frame_2(stream_reader): return frame -@pytest.fixture() +@pytest.fixture def sml_data_1_analyze(sml_data_1): r = SmlStreamReader() r.add(sml_data_1) @@ -56,30 +56,30 @@ def sml_data_1_analyze(sml_data_1): return '\n'.join(frame.get_analyze_str()) -@pytest.fixture() +@pytest.fixture def sml_frame_1_values(sml_frame_1): values = sml_frame_1.get_obis() return SmlFrameValues.create(0, values) -@pytest.fixture() +@pytest.fixture def sml_frame_1_analyze(sml_frame_1): return '\n'.join(sml_frame_1.get_analyze_str()) -@pytest.fixture() +@pytest.fixture def sml_frame_2_values(sml_frame_2): values = sml_frame_2.get_obis() return SmlFrameValues.create(0, values) -@pytest.fixture() +@pytest.fixture def sml_frame_2_analyze(sml_frame_2): return '\n'.join(sml_frame_2.get_analyze_str()) -@pytest.fixture() -def sml_data_1_analyze(): +@pytest.fixture +def sml_data_1_analyze() -> str: return ''' Received Frame -> b'760501188e6162006200726500000101760101070000000000000b00000000000000000000010163687700760501188e626200620072650000070177010b000000000000000000000172620165002ec3f47a77078181c78203ff010101010445425a0177070100000009ff010101010b000000000000000000000177070100010800ff6401018001621e52fb690000000a7ac1bc170177070100010801ff0101621e52fb690000000a74b1ea770177070100010802ff0101621e52fb6900000000060fd1a00177070100020800ff6401018001621e52fb69000000000d19e1c00177070100100700ff0101621b52fe55000089d90177070100240700ff0101621b52fe55000020220177070100380700ff0101621b52fe5500000a9201770701004c0700ff0101621b52fe5500005f2501010163810200760501188e636200620072650000020171016325fc00' diff --git a/tests/sml_device/frames/test_frame_1.py b/tests/sml_device/frames/test_frame_1.py index f7fd185..52d20e5 100644 --- a/tests/sml_device/frames/test_frame_1.py +++ b/tests/sml_device/frames/test_frame_1.py @@ -1,4 +1,3 @@ -from binascii import a2b_hex import pytest @@ -7,8 +6,8 @@ from sml2mqtt.sml_device import SmlDevice -@pytest.mark.ignore_log_errors() -async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_analyze, sml_frame_1_analyze): +@pytest.mark.ignore_log_errors +async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_analyze, sml_frame_1_analyze) -> None: device = SmlDevice('device_name') device.frame_handler = device.analyze_frame @@ -19,6 +18,7 @@ async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_1, msg = '\n'.join(x.msg for x in caplog.records) assert msg.removeprefix(sml_frame_1_analyze) == ''' +Using crc x25 Found none of the following obis ids in the sml frame: 0100000009ff, 01006001ffff Received Frame -> b'760500531efa620062007263010176010105001bb4fe0b0a0149534b0005020de272620165001bb32e620163a71400760500531efb620062007263070177010b0a0149534b0005020de2070100620affff72620165001bb32e757707010060320101010101010449534b0177070100600100ff010101010b0a0149534b0005020de20177070100010800ff65001c010401621e52ff650026bea90177070100020800ff0101621e52ff62000177070100100700ff0101621b52005301100101016350ba00760500531efc6200620072630201710163ba1900' @@ -27,8 +27,8 @@ async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_1, device_name/status: ERROR (QOS: 0, retain: False)''' -@pytest.mark.ignore_log_warnings() -async def test_frame_no_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_analyze, sml_frame_1_analyze): +@pytest.mark.ignore_log_warnings +async def test_frame_no_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_analyze, sml_frame_1_analyze) -> None: device = SmlDevice('device_name') device.frame_handler = device.analyze_frame @@ -39,6 +39,7 @@ async def test_frame_no_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_an msg = '\n'.join(x.msg for x in caplog.records) assert msg.removeprefix(sml_frame_1_analyze) == ''' +Using crc x25 Found obis id 0100600100ff in the sml frame No device found for 0a0149534b0005020de2 No filters found for 010060320101, creating default filters @@ -98,7 +99,7 @@ async def test_frame_no_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_an 0a0149534b0005020de2/status: SHUTDOWN (QOS: 0, retain: False)''' -async def test_frame_with_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_analyze, sml_frame_1_analyze): +async def test_frame_with_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_analyze, sml_frame_1_analyze) -> None: device = SmlDevice('device_name') device.frame_handler = device.analyze_frame @@ -114,6 +115,7 @@ async def test_frame_with_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_ print(msg) assert msg.removeprefix(sml_frame_1_analyze) == ''' +Using crc x25 Found obis id 0100600100ff in the sml frame Device found for 0a0149534b0005020de2 No filters found for 0100010800ff, creating default filters diff --git a/tests/sml_device/frames/test_frame_2.py b/tests/sml_device/frames/test_frame_2.py index ad334e2..7aa9cd9 100644 --- a/tests/sml_device/frames/test_frame_2.py +++ b/tests/sml_device/frames/test_frame_2.py @@ -1,4 +1,3 @@ -from binascii import a2b_hex import pytest @@ -6,8 +5,8 @@ from sml2mqtt.sml_device import SmlDevice -@pytest.mark.ignore_log_errors() -async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_2, arg_analyze, sml_frame_2_analyze): +@pytest.mark.ignore_log_errors +async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_2, arg_analyze, sml_frame_2_analyze) -> None: device = SmlDevice('device_name') device.frame_handler = device.analyze_frame @@ -18,6 +17,7 @@ async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_2, msg = '\n'.join(x.msg for x in caplog.records) assert msg.removeprefix(sml_frame_2_analyze) == ''' +Using crc x25 get_obis failed - try parsing frame Found none of the following obis ids in the sml frame: 0100000009ff, 01006001ffff Received Frame diff --git a/tests/sml_device/test_device.py b/tests/sml_device/test_device.py index 1c56594..9b22d05 100644 --- a/tests/sml_device/test_device.py +++ b/tests/sml_device/test_device.py @@ -1,12 +1,11 @@ -from binascii import a2b_hex import pytest from sml2mqtt.sml_device import SmlDevice -@pytest.mark.ignore_log_warnings() -async def test_device_analyze(no_mqtt, caplog, sml_data_1, arg_analyze, sml_data_1_analyze): +@pytest.mark.ignore_log_warnings +async def test_device_analyze(no_mqtt, caplog, sml_data_1, arg_analyze, sml_data_1_analyze) -> None: device = SmlDevice('device_name') device.frame_handler = device.analyze_frame @@ -34,6 +33,7 @@ async def test_device_analyze(no_mqtt, caplog, sml_data_1, arg_analyze, sml_data msg = '\n'.join(x.msg for x in filter(lambda x: x.name == 'sml.device_name', caplog.records)) assert msg.removeprefix(sml_data_1_analyze) == ''' +Using crc x25 Found obis id 0100000009ff in the sml frame No device found for 00000000000000000000 No filters found for 0100000009ff, creating default filters diff --git a/tests/sml_device/test_setup_device.py b/tests/sml_device/test_setup_device.py index ccc83c0..24d07f1 100644 --- a/tests/sml_device/test_setup_device.py +++ b/tests/sml_device/test_setup_device.py @@ -4,14 +4,12 @@ from sml2mqtt.config.config import GeneralSettings from sml2mqtt.config.device import SmlDeviceConfig, SmlValueConfig -from sml2mqtt.config.operations import OnChangeFilter from sml2mqtt.sml_device import SmlDevice from sml2mqtt.sml_device.setup_device import setup_device -from sml2mqtt.sml_value.operations import OnChangeFilterOperation -@pytest.mark.ignore_log_warnings() -def test_warnings(no_mqtt, caplog, sml_frame_1_values): +@pytest.mark.ignore_log_warnings +def test_warnings(no_mqtt, caplog, sml_frame_1_values) -> None: device = SmlDevice('test_device') device.mqtt_device.cfg.topic_full = 'test_device/device_id' diff --git a/tests/sml_device/test_watchdog.py b/tests/sml_device/test_watchdog.py index 73ce070..315900a 100644 --- a/tests/sml_device/test_watchdog.py +++ b/tests/sml_device/test_watchdog.py @@ -17,7 +17,7 @@ def get_watchdog() -> tuple[Mock, Watchdog]: return m, w -async def test_watchdog_expire(): +async def test_watchdog_expire() -> None: m, w = get_watchdog() w.start() @@ -34,7 +34,7 @@ async def test_watchdog_expire(): await asyncio.sleep(0.05) -async def test_watchdog_no_expire(): +async def test_watchdog_no_expire() -> None: m, w = get_watchdog() w.start() @@ -51,7 +51,7 @@ async def test_watchdog_no_expire(): await asyncio.sleep(0.05) -async def test_watchdog_setup_and_feed(sml_data_1): +async def test_watchdog_setup_and_feed(sml_data_1) -> None: obj = SmlDevice('test') obj.frame_handler = obj.process_frame diff --git a/tests/sml_values/test_operations/helper.py b/tests/sml_values/test_operations/helper.py index d794ef2..8ab4d8f 100644 --- a/tests/sml_values/test_operations/helper.py +++ b/tests/sml_values/test_operations/helper.py @@ -8,7 +8,7 @@ RE_ID = re.compile(r' at 0x[0-f]{6,}>') -def check_operation_repr(obj: ValueOperationBase, *values): +def check_operation_repr(obj: ValueOperationBase, *values) -> None: repr_str = RE_ID.sub('', repr(obj)) class_name = obj.__class__.__name__ @@ -24,7 +24,7 @@ def check_operation_repr(obj: ValueOperationBase, *values): assert target == repr_str, f'\n{target}\n{repr_str}' -def check_description(obj: ValueOperationBase, value: str | Iterable[str]): +def check_description(obj: ValueOperationBase, value: str | Iterable[str]) -> None: desc = list(obj.describe()) desc_text = '\n'.join(desc) if 'filter' in desc_text.lower(): diff --git a/tests/sml_values/test_operations/test_actions.py b/tests/sml_values/test_operations/test_actions.py index d2dce04..974b852 100644 --- a/tests/sml_values/test_operations/test_actions.py +++ b/tests/sml_values/test_operations/test_actions.py @@ -4,7 +4,7 @@ from sml2mqtt.sml_value.operations._helper import format_period -def test_format_period(): +def test_format_period() -> None: assert format_period(30.2) == '30.2 seconds' assert format_period(30) == '30 seconds' assert format_period(60) == '1 minute' @@ -14,7 +14,7 @@ def test_format_period(): assert format_period(3722) == '1 hour 2 minutes 2 seconds' -def test_refresh_action(monotonic): +def test_refresh_action(monotonic) -> None: f = RefreshActionOperation(30) check_operation_repr(f, '30s') check_description(f, '- Refresh Action: 30 seconds') @@ -33,7 +33,7 @@ def test_refresh_action(monotonic): assert f.process_value(None, None) == 2 -def test_heartbeat_action(monotonic): +def test_heartbeat_action(monotonic) -> None: f = HeartbeatActionOperation(30) check_operation_repr(f, '30s') diff --git a/tests/sml_values/test_operations/test_date_time.py b/tests/sml_values/test_operations/test_date_time.py index 8cbc711..3caeb2e 100644 --- a/tests/sml_values/test_operations/test_date_time.py +++ b/tests/sml_values/test_operations/test_date_time.py @@ -9,7 +9,7 @@ class PatchedNow: - def __init__(self): + def __init__(self) -> None: self.ret = None def set(self, dt: datetime): @@ -24,7 +24,7 @@ def __call__(self): class DateTimeFactory: def __init__(self, year: int | None = 2001, month: int | None = 1, day: int | None = None, hour: int | None = None, minute: int | None = None, second: int | None = 0, - microsecond: int | None = 0): + microsecond: int | None = 0) -> None: self.kwargs = { 'year': year, 'month': month, 'day': day, 'hour': hour, 'minute': minute, 'second': second, 'microsecond': microsecond @@ -47,7 +47,7 @@ def create(self, *args, **kwargs): return datetime(**call) -@pytest.fixture() +@pytest.fixture def now(monkeypatch): p = PatchedNow() monkeypatch.setattr(virtual_meter_module, 'get_now', p) @@ -55,7 +55,7 @@ def now(monkeypatch): return p -def test_finder_1(now): +def test_finder_1(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -67,7 +67,7 @@ def test_finder_1(now): assert f.calc_next() == dt_next.create(i) -def test_finder_dow(now): +def test_finder_dow(now) -> None: f = DateTimeFinder() f.add_time(time(2)) f.add_dow(1) @@ -80,7 +80,7 @@ def test_finder_dow(now): assert f.calc_next() == dt_next.create(i) -def test_finder_day(now): +def test_finder_day(now) -> None: f = DateTimeFinder() f.add_time(time(2)) f.add_day(15) @@ -102,7 +102,7 @@ def test_finder_day(now): assert f.calc_next() == dt_next.create(15, month=3) -def test_virtual_meter_start_now(now): +def test_virtual_meter_start_now(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -116,7 +116,7 @@ def test_virtual_meter_start_now(now): assert o.process_value(34, None) == 1 -def test_virtual_meter_start_normal(now): +def test_virtual_meter_start_normal(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -134,7 +134,7 @@ def test_virtual_meter_start_normal(now): assert o.process_value(35, None) == 1 -def test_virtual_meter_description(now): +def test_virtual_meter_description(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -158,7 +158,7 @@ def test_virtual_meter_description(now): ) -def test_virtual_meter_start_now_no_times(now): +def test_virtual_meter_start_now_no_times(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -172,7 +172,7 @@ def test_virtual_meter_start_now_no_times(now): assert o.process_value(34, None) == 1 -def test_virtual_meter_start_normal_no_times(now): +def test_virtual_meter_start_normal_no_times(now) -> None: f = DateTimeFinder() dt = DateTimeFactory(hour=1, minute=30) @@ -186,7 +186,7 @@ def test_virtual_meter_start_normal_no_times(now): assert o.process_value(34, None) == 1 -def test_virtual_meter_description_no_times(now): +def test_virtual_meter_description_no_times(now) -> None: f = DateTimeFinder() dt = DateTimeFactory(hour=1, minute=30) @@ -206,7 +206,7 @@ def test_virtual_meter_description_no_times(now): ) -def test_max_start_now(now): +def test_max_start_now(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -223,7 +223,7 @@ def test_max_start_now(now): assert o.process_value(50, None) is None -def test_max_start_normal(now): +def test_max_start_normal(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -245,7 +245,7 @@ def test_max_start_normal(now): assert o.process_value(50, None) is None -def test_max_description(now): +def test_max_description(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -293,7 +293,7 @@ def test_max_description(now): ) -def test_min_start_now(now): +def test_min_start_now(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -310,7 +310,7 @@ def test_min_start_now(now): assert o.process_value(49, None) is None -def test_min_start_normal(now): +def test_min_start_normal(now) -> None: f = DateTimeFinder() f.add_time(time(2)) @@ -332,7 +332,7 @@ def test_min_start_normal(now): assert o.process_value(49, None) is None -def test_min_description(now): +def test_min_description(now) -> None: f = DateTimeFinder() f.add_time(time(2)) diff --git a/tests/sml_values/test_operations/test_filter.py b/tests/sml_values/test_operations/test_filter.py index 17e17d2..f825f41 100644 --- a/tests/sml_values/test_operations/test_filter.py +++ b/tests/sml_values/test_operations/test_filter.py @@ -9,7 +9,7 @@ ) -def test_skip(): +def test_skip() -> None: f = SkipZeroMeterOperation() check_operation_repr(f) @@ -24,7 +24,7 @@ def test_skip(): ) -def test_delta(): +def test_delta() -> None: f = DeltaFilterOperation(min_value=5) check_operation_repr(f, 'min=5 min_percent=None') check_description(f, ['- Delta Filter:', ' Min : 5']) @@ -67,7 +67,7 @@ def test_delta(): assert f.process_value(15, None) == 15 -def test_on_change(): +def test_on_change() -> None: f = OnChangeFilterOperation() check_operation_repr(f) @@ -82,7 +82,7 @@ def test_on_change(): ) -def test_range(): +def test_range() -> None: # --------------------------------------------------------------------------------------------- # Min @@ -158,7 +158,7 @@ def test_range(): ) -def test_throttle_filter(monotonic): +def test_throttle_filter(monotonic) -> None: f = ThrottleFilterOperation(30) check_operation_repr(f, '30s') check_description(f, '- Throttle Filter: 30 seconds') diff --git a/tests/sml_values/test_operations/test_math.py b/tests/sml_values/test_operations/test_math.py index 8210f10..11e9d1d 100644 --- a/tests/sml_values/test_operations/test_math.py +++ b/tests/sml_values/test_operations/test_math.py @@ -3,12 +3,11 @@ from sml2mqtt.sml_value.operations import ( FactorOperation, OffsetOperation, - RangeFilterOperation, RoundOperation, ) -def test_factor(): +def test_factor() -> None: o = FactorOperation(5) check_operation_repr(o, '5') @@ -18,7 +17,7 @@ def test_factor(): assert o.process_value(-3, None) == -15 -def test_offset(): +def test_offset() -> None: o = OffsetOperation(-5) check_operation_repr(o, '-5') @@ -28,7 +27,7 @@ def test_offset(): assert o.process_value(-3, None) == -8 -def test_round(): +def test_round() -> None: o = RoundOperation(0) check_operation_repr(o, '0') @@ -48,7 +47,7 @@ def test_round(): assert o.process_value(-3.65, None) == -3.6 -def test_description(): +def test_description() -> None: check_description( FactorOperation(-5), '- Factor: -5' diff --git a/tests/sml_values/test_operations/test_operations.py b/tests/sml_values/test_operations/test_operations.py index 1621a74..ed4cb84 100644 --- a/tests/sml_values/test_operations/test_operations.py +++ b/tests/sml_values/test_operations/test_operations.py @@ -10,18 +10,18 @@ from sml2mqtt.sml_value.operations import OffsetOperation, OrOperation, SequenceOperation -def test_repr(): +def test_repr() -> None: check_operation_repr(OrOperation()) check_operation_repr(SequenceOperation()) class MockOperationsGroup: - def __init__(self, operation: ValueOperationBase): + def __init__(self, operation: ValueOperationBase) -> None: self.sentinel = object() self.operation: Final = operation self.mocks: list[Mock] = [] - def assert_called(self, *args: float | Literal['-']): + def assert_called(self, *args: float | Literal['-']) -> None: for mock, arg in zip(self.mocks, args, strict=True): # type: Mock, float | Literal['-'] if arg == '-': mock.assert_not_called() @@ -55,25 +55,25 @@ def get_mock_group(cls: type[OrOperation | SequenceOperation | SmlValue], *retur return m -def test_or_no_exit(): +def test_or_no_exit() -> None: m = get_mock_group(OrOperation, None, None, None) assert m.process_value(1) is None m.assert_called(1, 1, 1) -def test_or_last_exit(): +def test_or_last_exit() -> None: m = get_mock_group(OrOperation, None, None, 5) assert m.process_value(1) == 5 m.assert_called(1, 1, 1) -def test_or_first_exit(): +def test_or_first_exit() -> None: m = get_mock_group(OrOperation, 3, 99, 77) assert m.process_value(1) == 3 m.assert_called(1, 1, 1) -def test_or_single(): +def test_or_single() -> None: m = get_mock_group(OrOperation, None) assert m.process_value(1) is None m.assert_called(1) @@ -83,7 +83,7 @@ def test_or_single(): m.assert_called(1) -def test_or_description(): +def test_or_description() -> None: o = OrOperation() o.add_operation(OffsetOperation(3)) @@ -109,28 +109,28 @@ def test_or_description(): @pytest.mark.parametrize('cls', [SequenceOperation, SmlValue]) -def test_seq_no_exit(cls): +def test_seq_no_exit(cls) -> None: m = get_mock_group(cls, 1, 2, 3) assert m.process_value(0) == 3 m.assert_called(0, 1, 2) @pytest.mark.parametrize('cls', [SequenceOperation, SmlValue]) -def test_seq_first_exit(cls): +def test_seq_first_exit(cls) -> None: m = get_mock_group(cls, None, 2, None) assert m.process_value(1) is None m.assert_called(1, None, 2) @pytest.mark.parametrize('cls', [SequenceOperation, SmlValue]) -def test_seq_last_exit(cls): +def test_seq_last_exit(cls) -> None: m = get_mock_group(cls, 1, 2, None) assert m.process_value(0) is None m.assert_called(0, 1, 2) @pytest.mark.parametrize('cls', [SequenceOperation, SmlValue]) -def test_seq_single(cls): +def test_seq_single(cls) -> None: m = get_mock_group(cls, None) assert m.process_value(1) is None m.assert_called(1) @@ -140,7 +140,7 @@ def test_seq_single(cls): m.assert_called(1) -def test_sequence_description(): +def test_sequence_description() -> None: o = SequenceOperation() o.add_operation(OffsetOperation(3)) diff --git a/tests/sml_values/test_operations/test_time_series.py b/tests/sml_values/test_operations/test_time_series.py index 766b97b..0694c5e 100644 --- a/tests/sml_values/test_operations/test_time_series.py +++ b/tests/sml_values/test_operations/test_time_series.py @@ -13,7 +13,7 @@ def info(timestamp: int): return SmlValueInfo(None, SmlFrameValues.create(timestamp, []), 0) -def test_max(): +def test_max() -> None: o = MaxOfIntervalOperation(TimeSeries(5), False) check_operation_repr(o, 'interval=5s') @@ -55,7 +55,7 @@ def test_max(): ) -def test_min(): +def test_min() -> None: o = MinOfIntervalOperation(TimeSeries(5), False) check_operation_repr(o, 'interval=5s') @@ -76,7 +76,7 @@ def test_min(): ) -def test_mean(): +def test_mean() -> None: o = MeanOfIntervalOperation(TimeSeries(10), False) check_operation_repr(o, 'interval=10s') diff --git a/tests/sml_values/test_operations/test_workarounds.py b/tests/sml_values/test_operations/test_workarounds.py index b9d4af0..5a467c9 100644 --- a/tests/sml_values/test_operations/test_workarounds.py +++ b/tests/sml_values/test_operations/test_workarounds.py @@ -24,17 +24,17 @@ def get_info() -> SmlValueInfo: return SmlValueInfo(power, SmlFrameValues.create(0, [power, energy]), 0) -def test_repr(): +def test_repr() -> None: o = NegativeOnEnergyMeterWorkaroundOperation() check_operation_repr(o, '0100010800ff') -def test_none(): +def test_none() -> None: o = NegativeOnEnergyMeterWorkaroundOperation() assert o.process_value(None, get_info()) is None -def test_make_negative(): +def test_make_negative() -> None: o = NegativeOnEnergyMeterWorkaroundOperation() info = get_info() @@ -43,7 +43,7 @@ def test_make_negative(): assert o.process_value(value, info) == -4189.0 -def test_keep_positive(): +def test_keep_positive() -> None: o = NegativeOnEnergyMeterWorkaroundOperation() info = get_info() @@ -55,7 +55,7 @@ def test_keep_positive(): assert o.process_value(value, info) == 4189.0 -def test_description(): +def test_description() -> None: check_description( NegativeOnEnergyMeterWorkaroundOperation(), '- Negative On Status Of Energy Meter 0100010800ff' diff --git a/tests/sml_values/test_setup_operations.py b/tests/sml_values/test_setup_operations.py index 873c5f9..732b578 100644 --- a/tests/sml_values/test_setup_operations.py +++ b/tests/sml_values/test_setup_operations.py @@ -8,7 +8,7 @@ import sml2mqtt.config.operations as operations_module from sml2mqtt.config.operations import Offset, OperationsModels, Sequence -from sml2mqtt.sml_value.operations import OffsetOperation, SequenceOperation, VirtualMeterOperation +from sml2mqtt.sml_value.operations import SequenceOperation, VirtualMeterOperation from sml2mqtt.sml_value.setup_operations import MAPPING, get_kwargs_names, setup_operations @@ -97,7 +97,7 @@ def test_all_models_in_mapping(): raise ValueError(msg) -def test_simple(): +def test_simple() -> None: cfg = Sequence(sequence=[ Offset(offset=5) ]) @@ -112,7 +112,7 @@ def test_simple(): ] -def test_virtual_meter(): +def test_virtual_meter() -> None: cfg = Sequence(sequence=[ {'type': 'meter', 'start now': True, 'reset times': ['02:00'], 'reset days': ['mon', 6]}, @@ -131,7 +131,7 @@ def test_virtual_meter(): assert o._dt_finder.dows == (1, ) -def test_complex(): +def test_complex() -> None: cfg = Sequence(sequence=[ {'offset': 5}, {'or': [{'offset': 5}, {'factor': 3}]} diff --git a/tests/sml_values/test_values.py b/tests/sml_values/test_values.py index e2182bb..b0d1f24 100644 --- a/tests/sml_values/test_values.py +++ b/tests/sml_values/test_values.py @@ -14,7 +14,7 @@ from sml_values.test_operations.helper import check_description -def test_values(sml_frame_1_values: SmlFrameValues, no_mqtt): +def test_values(sml_frame_1_values: SmlFrameValues, no_mqtt) -> None: mqtt = MqttObj(topic_fragment='test', qos=0, retain=False).update() v = SmlValues() @@ -64,8 +64,8 @@ def get_error_message(e: Sml2MqttExceptionWithLog, caplog) -> list[str]: return msgs -@pytest.mark.ignore_log_errors() -def test_too_much(sml_frame_1_values: SmlFrameValues, no_mqtt, caplog): +@pytest.mark.ignore_log_errors +def test_too_much(sml_frame_1_values: SmlFrameValues, no_mqtt, caplog) -> None: v = SmlValues() v.set_skipped('010060320101', '0100600100ff') @@ -93,8 +93,8 @@ def test_too_much(sml_frame_1_values: SmlFrameValues, no_mqtt, caplog): ] -@pytest.mark.ignore_log_errors() -def test_missing(sml_frame_1_values: SmlFrameValues, no_mqtt, caplog): +@pytest.mark.ignore_log_errors +def test_missing(sml_frame_1_values: SmlFrameValues, no_mqtt, caplog) -> None: v = SmlValues() v.set_skipped('010060320101', '0100600100ff', '0100020800ff', '0100010800ff', '0100100700ff') diff --git a/tests/test_docs.py b/tests/test_docs.py index 7211877..a7a2cc4 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -96,16 +96,16 @@ def validate_yaml_blocks(file: Path, prefix_model: str = 'yamlmodel: ', func: Ca obj.validate(func) -def test_yaml_samples(pytestconfig): +def test_yaml_samples(pytestconfig) -> None: class DummyOperationParent: - def add_operation(self, obj): + def add_operation(self, obj) -> None: pass class HasOperationsModel(BaseModel): operations: list[BaseModel] - def check_obj(model: BaseModel): + def check_obj(model: BaseModel) -> None: if model.__class__ in MAPPING: setup_operations(DummyOperationParent(), HasOperationsModel(operations=[model])) @@ -114,7 +114,7 @@ def check_obj(model: BaseModel): validate_yaml_blocks(file, func=check_obj) -def _get_documented_objs(path: Path, objs: set[str]): +def _get_documented_objs(path: Path, objs: set[str]) -> None: current_module = '' @@ -132,7 +132,7 @@ def _get_documented_objs(path: Path, objs: set[str]): objs.add(obj_name) -def test_config_documentation_complete(pytestconfig): +def test_config_documentation_complete(pytestconfig) -> None: cfg_model_dir: Path = pytestconfig.rootpath / 'src' / 'sml2mqtt' / 'config' assert cfg_model_dir.is_dir() diff --git a/tests/test_mqtt_obj.py b/tests/test_mqtt_obj.py index 3ad96e4..a0f22a1 100644 --- a/tests/test_mqtt_obj.py +++ b/tests/test_mqtt_obj.py @@ -3,7 +3,7 @@ from sml2mqtt.mqtt import MqttObj, check_for_duplicate_topics -def test_topmost(monkeypatch): +def test_topmost(monkeypatch) -> None: parent = MqttObj('base', 2, True).update() assert parent.topic == 'base' @@ -16,14 +16,14 @@ def test_topmost(monkeypatch): assert parent.retain is True -def test_prefix_empty(monkeypatch): +def test_prefix_empty(monkeypatch) -> None: parent = MqttObj('', 2, True).update() child = parent.create_child('child') assert (child.topic, child.qos, child.retain) == ('child', 2, True) -def test_child_change(monkeypatch): +def test_child_change(monkeypatch) -> None: parent = MqttObj('base', 2, True).update() child = parent.create_child('child') @@ -52,8 +52,8 @@ def test_child_change(monkeypatch): assert (child.topic, child.qos, child.retain) == ('base/child', 0, False) -@pytest.mark.ignore_log_warnings() -def test_check_for_duplicate_messages(caplog): +@pytest.mark.ignore_log_warnings +def test_check_for_duplicate_messages(caplog) -> None: parent = MqttObj('base', 2, True).update() parent.create_child('child') parent.create_child('child') diff --git a/tests/test_readme.py b/tests/test_readme.py index 92984e5..a6f6358 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -5,7 +5,7 @@ from sml2mqtt.config.config import Settings -def test_readme(pytestconfig): +def test_readme(pytestconfig) -> None: readme = pytestconfig.rootpath / 'readme.md' assert readme.is_file() diff --git a/tests/test_source/conftest.py b/tests/test_source/conftest.py index 16c46a7..0a467ac 100644 --- a/tests/test_source/conftest.py +++ b/tests/test_source/conftest.py @@ -7,7 +7,7 @@ class DeviceMock(DeviceProto): - def __init__(self): + def __init__(self) -> None: self.on_source_data = Mock() self.on_source_failed = Mock() self.on_error = Mock() @@ -17,7 +17,7 @@ def name(self) -> str: return f'DeviceMock at 0x{id(self):x}' -@pytest.fixture() +@pytest.fixture def device_mock() -> DeviceMock: m = DeviceMock() m.on_source_data.assert_not_called() diff --git a/tests/test_source/test_create.py b/tests/test_source/test_create.py index 15775c9..238761d 100644 --- a/tests/test_source/test_create.py +++ b/tests/test_source/test_create.py @@ -5,7 +5,7 @@ from sml2mqtt.sml_source.http import HttpSource -async def test_create_http_no_auth(device_mock): +async def test_create_http_no_auth(device_mock) -> None: cfg = HttpSourceSettings(type='http', url='http://localhost/a', interval=3, timeout=9) obj = await create_source(device_mock, cfg) @@ -19,7 +19,7 @@ async def test_create_http_no_auth(device_mock): device_mock.on_error.assert_not_called() -async def test_create_http_auth(device_mock): +async def test_create_http_auth(device_mock) -> None: cfg = HttpSourceSettings(type='http', url='http://localhost/a', interval=3, timeout=9, user='u', password='p') obj = await create_source(device_mock, cfg) diff --git a/tests/test_source/test_http.py b/tests/test_source/test_http.py index 31d0e29..6c76b6e 100644 --- a/tests/test_source/test_http.py +++ b/tests/test_source/test_http.py @@ -10,13 +10,13 @@ from sml2mqtt.sml_source.http import HttpSource, close_session -@pytest.fixture() +@pytest.fixture def source(device_mock): return HttpSource(device_mock, 'http://localhost:39999', interval=0.020, auth=None, timeout=ClientTimeout(0.5)) @pytest.mark.skipif(sys.platform.lower() != 'win32', reason="It's a mystery why this fails in CI") -async def test_200(sml_data_1, device_mock, source): +async def test_200(sml_data_1, device_mock, source) -> None: with aioresponses() as m: m.get(source.url, body=sml_data_1) @@ -35,7 +35,7 @@ async def test_200(sml_data_1, device_mock, source): @pytest.mark.skipif(sys.platform.lower() != 'win32', reason="It's a mystery why this fails in CI") -async def test_400_then_200(sml_data_1, device_mock, source): +async def test_400_then_200(sml_data_1, device_mock, source) -> None: with aioresponses() as m: m.get(source.url, status=404) @@ -56,7 +56,7 @@ async def test_400_then_200(sml_data_1, device_mock, source): @pytest.mark.skipif(sys.platform.lower() != 'win32', reason="It's a mystery why this fails in CI") -async def test_400(device_mock, source): +async def test_400(device_mock, source) -> None: with aioresponses() as m: for _ in range(10): @@ -75,12 +75,12 @@ async def test_400(device_mock, source): await close_session() -def test_error_repr(): +def test_error_repr() -> None: assert str(HttpStatusError(404)) == 'HttpStatusError: 404' @pytest.mark.skipif(sys.platform.lower() != 'win32', reason="It's a mystery why this fails in CI") -async def test_timeout(device_mock, source): +async def test_timeout(device_mock, source) -> None: e = TimeoutError() diff --git a/tox.ini b/tox.ini index 374d97b..fc4e20f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,13 +5,14 @@ envlist = py311 py312 docs + slotscheck [gh-actions] python = 3.10: py310, docs 3.11: py311 - 3.12: py312 + 3.12: py312, slotscheck [testenv] deps = @@ -35,6 +36,14 @@ commands = allowlist_externals = mkdir +[testenv:slotscheck] +deps = + -r{toxinidir}/requirements.txt + slotscheck +change_dir = {toxinidir}/src +commands = + python -m slotscheck sml2mqtt --verbose + [pytest] asyncio_mode = auto