From dfe2496b2750b0707351bb6965392b049b01712d Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 12:20:50 +0000 Subject: [PATCH 01/10] Get the devcontainer building for HA 2025.1.0 --- .devcontainer/devcontainer.json | 2 +- requirements.txt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 632af1a8..825ee3ee 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye", + "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12-bullseye", "name": "Foxess Modbus Container", "appPort": ["9123:8123"], "postCreateCommand": ".devcontainer/setup", diff --git a/requirements.txt b/requirements.txt index 0ae6a848..8a24a436 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -pip>=21.0,<23.2 -homeassistant==2023.7.0 +# pip>=21.0,<23.2 +homeassistant==2025.1.0 # Testing -pytest-homeassistant-custom-component==0.13.42 # Matching version for 2023.7.0 +pytest-homeassistant-custom-component==0.13.201 # Matching version for 2025.1.0 psutil-home-assistant # Not sure why this is needed? fnv_hash_fast # Or this? pytest-asyncio @@ -14,7 +14,7 @@ ruff==0.0.275 # These are duplicated in .pre-commit-config.yaml reorder-python-imports==3.10.0 mypy==1.4.1 -homeassistant-stubs==2023.7.0 # Matching HA version +# homeassistant-stubs==2025.1.0 # Matching HA version types-python-slugify==8.0.0.2 voluptuous-stubs==0.1.1 # For mypy. Keep in sync with manifest.json and https://github.com/home-assistant/core/blob/master/requirements_all.txt. From a0aa11a079ff9b2ac8d48f76cd722a1291861cb6 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 12:58:38 +0000 Subject: [PATCH 02/10] Update pymodbus to 3.7.4 and fix breaks --- .../foxess_modbus/client/modbus_client.py | 31 ++++++++++--------- custom_components/foxess_modbus/manifest.json | 2 +- hacs.json | 2 +- requirements.txt | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/custom_components/foxess_modbus/client/modbus_client.py b/custom_components/foxess_modbus/client/modbus_client.py index da565a12..78102528 100644 --- a/custom_components/foxess_modbus/client/modbus_client.py +++ b/custom_components/foxess_modbus/client/modbus_client.py @@ -13,13 +13,12 @@ from homeassistant.core import HomeAssistant from pymodbus.client import ModbusSerialClient from pymodbus.client import ModbusUdpClient -from pymodbus.pdu import ModbusResponse -from pymodbus.register_read_message import ReadHoldingRegistersResponse -from pymodbus.register_read_message import ReadInputRegistersResponse -from pymodbus.register_write_message import WriteMultipleRegistersResponse -from pymodbus.register_write_message import WriteSingleRegisterResponse -from pymodbus.transaction import ModbusRtuFramer -from pymodbus.transaction import ModbusSocketFramer +from pymodbus.framer import FramerType +from pymodbus.pdu import ModbusPDU +from pymodbus.pdu.register_read_message import ReadHoldingRegistersResponse +from pymodbus.pdu.register_read_message import ReadInputRegistersResponse +from pymodbus.pdu.register_write_message import WriteMultipleRegistersResponse +from pymodbus.pdu.register_write_message import WriteSingleRegisterResponse from .. import client from ..common.types import ConnectionType @@ -39,19 +38,19 @@ _CLIENTS: dict[str, dict[str, Any]] = { SERIAL: { "client": ModbusSerialClient, - "framer": ModbusRtuFramer, + "framer": FramerType.RTU, }, TCP: { "client": CustomModbusTcpClient, - "framer": ModbusSocketFramer, + "framer": FramerType.SOCKET, }, UDP: { "client": ModbusUdpClient, - "framer": ModbusSocketFramer, + "framer": FramerType.SOCKET, }, RTU_OVER_TCP: { "client": CustomModbusTcpClient, - "framer": ModbusRtuFramer, + "framer": FramerType.RTU, }, } @@ -70,14 +69,16 @@ def __init__(self, hass: HomeAssistant, protocol: str, adapter: InverterAdapter, client = _CLIENTS[protocol] - # Delaying for a second after establishing a connection seems to help the inverter stability, - # see https://github.com/nathanmarlor/foxess_modbus/discussions/132 config = { **config, "framer": client["framer"], - "delay_on_connect": 1 if adapter.connection_type == ConnectionType.LAN else None, } + # Delaying for a second after establishing a connection seems to help the inverter stability, + # see https://github.com/nathanmarlor/foxess_modbus/discussions/132 + if adapter.connection_type == ConnectionType.LAN: + config["delay_on_connect"] = 1 + # If our custom PosixPollSerial hack is supported, use that. This uses poll rather than select, which means we # don't break when there are more than 1024 fds. See #457. # Only supported on posix, see https://github.com/pyserial/pyserial/blob/7aeea35429d15f3eefed10bbb659674638903e3a/serial/__init__.py#L31 @@ -226,7 +227,7 @@ def __str__(self) -> str: class ModbusClientFailedError(Exception): """Raised when the ModbusClient fails to read/write""" - def __init__(self, message: str, client: ModbusClient, response: ModbusResponse | Exception) -> None: + def __init__(self, message: str, client: ModbusClient, response: ModbusPDU | Exception) -> None: super().__init__(f"{message} from {client}: {response}") self.message = message self.client = client diff --git a/custom_components/foxess_modbus/manifest.json b/custom_components/foxess_modbus/manifest.json index 179810e3..9d774a57 100755 --- a/custom_components/foxess_modbus/manifest.json +++ b/custom_components/foxess_modbus/manifest.json @@ -8,6 +8,6 @@ "integration_type": "service", "iot_class": "local_push", "issue_tracker": "https://github.com/nathanmarlor/foxess_modbus/issues", - "requirements": ["pymodbus>=3.1.3"], + "requirements": ["pymodbus>=3.7.4"], "version": "1.0.0" } diff --git a/hacs.json b/hacs.json index b0c6fe49..67391f2c 100755 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "FoxESS - Modbus", "hacs": "1.20.0", - "homeassistant": "2023.7.0", + "homeassistant": "2025.1.0", "render_readme": true } diff --git a/requirements.txt b/requirements.txt index 8a24a436..2ab59eaa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,5 @@ types-python-slugify==8.0.0.2 voluptuous-stubs==0.1.1 # For mypy. Keep in sync with manifest.json and https://github.com/home-assistant/core/blob/master/requirements_all.txt. # If changed, make sure subclasses in modbus_client are still valid! -pymodbus==3.5.4 +pymodbus==3.7.4 pyserial==3.5 From 1015c897570c8d8d56789d6307a0ef02379eb758 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 13:11:53 +0000 Subject: [PATCH 03/10] Remove metaclass workaround which depends on HA version Since we're now locked into 2025.1.0, we can remove version switching. --- .../foxess_modbus/entities/entity_factory.py | 27 ++++++++----------- .../entities/modbus_entity_mixin.py | 15 +++-------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/custom_components/foxess_modbus/entities/entity_factory.py b/custom_components/foxess_modbus/entities/entity_factory.py index d3cab5fa..3ebe3e46 100644 --- a/custom_components/foxess_modbus/entities/entity_factory.py +++ b/custom_components/foxess_modbus/entities/entity_factory.py @@ -13,27 +13,22 @@ from ..common.types import RegisterType from .inverter_model_spec import InverterModelSpec + # HA introduced a FrozenOrThawed metaclass which is used by EntityDescription. # This conflicts with ABC's metaclass. -# If EntityDescription has a metaclass (FrozenOrThawed), we need to combine that with -# ABC's metaclass, see https://github.com/nathanmarlor/foxess_modbus/issues/480. -# This is to allow HA to move to frozen entity descriptions (to aid caching), and will -# start logging deprecation warnings in 2024.x. -if type(EntityDescription) == type(type): # type: ignore - _METACLASS = type(ABC) - ENTITY_DESCRIPTION_KWARGS = {} -else: - - class EntityFactoryMetaclass(type(EntityDescription), type(ABC)): # type: ignore - """ - Metaclass to use for EntityFactory. - """ +# We need to combine EntityDescription's metaclass with ABC's metaclass, see +# https://github.com/nathanmarlor/foxess_modbus/issues/480. This is to allow HA to move to frozen entity descriptions +# (to aid caching), and will start logging deprecation warnings in 2024.x. +class EntityFactoryMetaclass(type(EntityDescription), type(ABC)): # type: ignore + """ + Metaclass to use for EntityFactory. + """ + - _METACLASS = EntityFactoryMetaclass - ENTITY_DESCRIPTION_KWARGS = {"frozen": True} +ENTITY_DESCRIPTION_KWARGS = {"frozen": True} -class EntityFactory(ABC, metaclass=_METACLASS): # type: ignore +class EntityFactory(ABC, metaclass=EntityFactoryMetaclass): # type: ignore """Factory which can create entities""" @property diff --git a/custom_components/foxess_modbus/entities/modbus_entity_mixin.py b/custom_components/foxess_modbus/entities/modbus_entity_mixin.py index 1b2586a5..7a31d330 100644 --- a/custom_components/foxess_modbus/entities/modbus_entity_mixin.py +++ b/custom_components/foxess_modbus/entities/modbus_entity_mixin.py @@ -1,7 +1,6 @@ """Mixin providing common functionality for all entity classes""" import logging -from abc import ABC from typing import TYPE_CHECKING from typing import Any from typing import Protocol @@ -73,21 +72,15 @@ class ModbusEntityProtocol(Protocol): else: _ModbusEntityMixinBase = object + # HA introduced a ABCCachedProperties metaclass which is used by Entity, and which derives from ABCMeta. # This conflicts with Protocol's metaclass (from ModbusEntityProtocol). -if type(Entity) == type(ABC): - _METACLASS = type(Entity) - -else: - - class ModbusEntityMixinMetaclass(type(Entity), type(Protocol)): # type: ignore - pass - - _METACLASS = ModbusEntityMixinMetaclass +class ModbusEntityMixinMetaclass(type(Entity), type(Protocol)): # type: ignore + pass class ModbusEntityMixin( - ModbusControllerEntity, ModbusEntityProtocol, _ModbusEntityMixinBase, metaclass=_METACLASS # type: ignore + ModbusControllerEntity, ModbusEntityProtocol, _ModbusEntityMixinBase, metaclass=ModbusEntityMixinMetaclass ): """ Mixin for subclasses of Entity From ac6007d0a110bc4888f25e75bd3ef687e0957910 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 13:16:11 +0000 Subject: [PATCH 04/10] Remove version-dependent workaround for pymodbus's client.connected property --- custom_components/foxess_modbus/client/modbus_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/custom_components/foxess_modbus/client/modbus_client.py b/custom_components/foxess_modbus/client/modbus_client.py index 78102528..8b6f01ee 100644 --- a/custom_components/foxess_modbus/client/modbus_client.py +++ b/custom_components/foxess_modbus/client/modbus_client.py @@ -200,12 +200,10 @@ async def _async_pymodbus_call(self, call: Callable[..., T], *args: Any, auto_co """Convert async to sync pymodbus call.""" def _call() -> T: - # pymodbus 3.4.1 removes automatic reconnections for the sync modbus client. - # However, in versions prior to 4.3.0, the ModbusUdpClient didn't have a connected property. # When using pollserial://, connected calls into serial.serial_for_url, which calls importlib.import_module, # which HA doesn't like (see https://github.com/nathanmarlor/foxess_modbus/issues/618). # Therefore we need to do this check inside the executor job - if auto_connect and hasattr(self._client, "connected") and not self._client.connected: + if auto_connect and not self._client.connected: self._client.connect() # If the connection failed, this call will throw an appropriate error return call(*args) From 0677ecc4363ac5be4975bf66cb134efe76880d3d Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 13:18:56 +0000 Subject: [PATCH 05/10] Bump python version to 3.13 to remove HA warning --- .devcontainer/devcontainer.json | 2 +- .github/workflows/{tests.yaml => tests.yml} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{tests.yaml => tests.yml} (98%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 825ee3ee..16b515b6 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12-bullseye", + "image": "mcr.microsoft.com/vscode/devcontainers/python:3.13-bullseye", "name": "Foxess Modbus Container", "appPort": ["9123:8123"], "postCreateCommand": ".devcontainer/setup", diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yml similarity index 98% rename from .github/workflows/tests.yaml rename to .github/workflows/tests.yml index 937c84c3..ea575de7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ on: - cron: "0 0 * * *" env: - DEFAULT_PYTHON: "3.11" + DEFAULT_PYTHON: "3.13" jobs: pre-commit: From 4c73185e5d3d0a84b3fd1057e3bd1d04aedee389 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 13:19:10 +0000 Subject: [PATCH 06/10] We don't need to manually upgrade syrupy any more --- .devcontainer/setup | 3 --- .github/workflows/tests.yml | 3 --- requirements.txt | 1 + 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.devcontainer/setup b/.devcontainer/setup index fa632790..880325f0 100644 --- a/.devcontainer/setup +++ b/.devcontainer/setup @@ -5,8 +5,5 @@ set -e cd "$(dirname "$0")/.." python3 -m pip install -r requirements.txt -# json files on syrupy are broken before 4.0.4, but pytest-homeassistant-custom-component for our current HA version -# relies on 4.0.2 -python3 -m pip install --no-deps syrupy==4.0.4 git config --global --fixed-value --replace-all safe.directory "${PWD}" "${PWD}" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea575de7..9968ca7c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,9 +29,6 @@ jobs: - name: Install Python modules run: | pip install --no-cache-dir -r requirements.txt - # json files on syrupy are broken before 4.0.4, but pytest-homeassistant-custom-component for our current HA - # version relies on 4.0.2 - pip install --no-deps syrupy==4.0.4 - name: Run pre-commit on all files run: | diff --git a/requirements.txt b/requirements.txt index 2ab59eaa..fb6135de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ ruff==0.0.275 # These are duplicated in .pre-commit-config.yaml reorder-python-imports==3.10.0 mypy==1.4.1 +# Currently not avaiable, see https://github.com/KapJI/homeassistant-stubs/issues/510 # homeassistant-stubs==2025.1.0 # Matching HA version types-python-slugify==8.0.0.2 voluptuous-stubs==0.1.1 From d1e87f16b0b318a4b6895f864389f1771b945bf8 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 13:34:34 +0000 Subject: [PATCH 07/10] Update CustomModbusTcpClient to match upstream --- .../client/custom_modbus_tcp_client.py | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/custom_components/foxess_modbus/client/custom_modbus_tcp_client.py b/custom_components/foxess_modbus/client/custom_modbus_tcp_client.py index 0b76c1ab..95f3be77 100644 --- a/custom_components/foxess_modbus/client/custom_modbus_tcp_client.py +++ b/custom_components/foxess_modbus/client/custom_modbus_tcp_client.py @@ -14,14 +14,14 @@ class CustomModbusTcpClient(ModbusTcpClient): """Custom ModbusTcpClient subclass with some hacks""" - def __init__(self, delay_on_connect: int | None, **kwargs: Any) -> None: + def __init__(self, delay_on_connect: int | None = None, **kwargs: Any) -> None: super().__init__(**kwargs) self._delay_on_connect = delay_on_connect def connect(self) -> bool: was_connected = self.socket is not None if not was_connected: - _LOGGER.debug("Connecting to %s", self.params) + _LOGGER.debug("Connecting to %s", self.comm_params) is_connected = cast(bool, super().connect()) # pymodbus doesn't disable Nagle's algorithm. This slows down reads quite substantially as the # TCP stack waits to see if we're going to send anything else. Disable it ourselves. @@ -34,7 +34,7 @@ def connect(self) -> bool: # Replacement of ModbusTcpClient to use poll rather than select, see # https://github.com/nathanmarlor/foxess_modbus/issues/275 - def recv(self, size: int) -> bytes: + def recv(self, size: int | None) -> bytes: """Read data from the underlying descriptor.""" super(ModbusTcpClient, self).recv(size) if not self.socket: @@ -48,13 +48,9 @@ def recv(self, size: int) -> bytes: # is received or timeout is expired. # If timeout expires returns the read data, also if its length is # less than the expected size. - self.socket.setblocking(0) + self.socket.setblocking(False) - # In the base method this is 'timeout = self.comm_params.timeout', but that changed from 'self.params.timeout' - # in 3.4.1. So we don't have a consistent way to access the timeout. - # However, this just mirrors what we set, which is the default of 3s. So use that. - # Annoyingly 3.4.1 - timeout = 3 + timeout = self.comm_params.timeout_connect or 0 # If size isn't specified read up to 4096 bytes at a time. if size is None: @@ -94,20 +90,5 @@ def recv(self, size: int) -> bytes: if time_ > end: break + self.last_frame_end = round(time.time(), 6) return b"".join(data) - - # Replacement of ModbusTcpClient to use poll rather than select, see - # https://github.com/nathanmarlor/foxess_modbus/issues/275 - def _check_read_buffer(self) -> bytes | None: - """Check read buffer.""" - time_ = time.time() - end = time_ + self.params.timeout - data = None - - assert self.socket is not None - poll = select.poll() - poll.register(self.socket, select.POLLIN) - poll_res = poll.poll(end - time_) - if len(poll_res) > 0: - data = self.socket.recv(1024) - return data From 872164f324c4d213b92c3960e0af949daab13385 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 13:34:53 +0000 Subject: [PATCH 08/10] Update mypy and black --- .pre-commit-config.yaml | 4 ++-- requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5194ca6c..522dcee7 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,13 +24,13 @@ repos: hooks: - id: ruff - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.5.1 hooks: - id: mypy # These are duplicated from requirements.txt additional_dependencies: [ - homeassistant-stubs==2023.7.0, + homeassistant-stubs==2024.12.5, types-python-slugify==8.0.0.2, voluptuous-stubs==0.1.1, ] diff --git a/requirements.txt b/requirements.txt index fb6135de..6c9ffa51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,11 +9,11 @@ pytest-asyncio colorlog==6.7.0 pre-commit==3.3.3 -black==23.3.0 +black==23.9.0 ruff==0.0.275 # These are duplicated in .pre-commit-config.yaml reorder-python-imports==3.10.0 -mypy==1.4.1 +mypy==1.5.1 # Currently not avaiable, see https://github.com/KapJI/homeassistant-stubs/issues/510 # homeassistant-stubs==2025.1.0 # Matching HA version types-python-slugify==8.0.0.2 From 4fc390d2236d4535de35895e8e104de4e72261c2 Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 13:40:24 +0000 Subject: [PATCH 09/10] Remove HA version-specific workaround for IntegrationSensor --- .../entities/modbus_integration_sensor.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/custom_components/foxess_modbus/entities/modbus_integration_sensor.py b/custom_components/foxess_modbus/entities/modbus_integration_sensor.py index 04171a85..c345fbfe 100644 --- a/custom_components/foxess_modbus/entities/modbus_integration_sensor.py +++ b/custom_components/foxess_modbus/entities/modbus_integration_sensor.py @@ -1,6 +1,5 @@ """Sensor""" -import inspect import logging from dataclasses import dataclass from datetime import timedelta @@ -26,18 +25,7 @@ _LOGGER = logging.getLogger(__name__) - -def _make_integration_sensor_kwargs() -> dict[str, Any]: - # HA 2024.7 introduced a new non-optional max_sub_interval parameter - kwargs: dict[str, Any] = {} - args = inspect.signature(IntegrationSensor.__init__) - if "max_sub_interval" in args.parameters: - kwargs["max_sub_interval"] = timedelta(minutes=1) # Default used by integration sensor config - - return kwargs - - -_INTEGRATION_SENSOR_KWARGS = _make_integration_sensor_kwargs() +MAX_SUB_INTERVAL = timedelta(minutes=1) # Default used by integration sensor config @dataclass(kw_only=True, **ENTITY_DESCRIPTION_KWARGS) @@ -121,7 +109,7 @@ def __init__( unique_id=None, unit_prefix=None, unit_time=unit_time, - **_INTEGRATION_SENSOR_KWARGS, + max_sub_interval=MAX_SUB_INTERVAL, ) # Use the icon from entity_description From 7d656f225d0ebfd065bebf4f02170a4944980cfd Mon Sep 17 00:00:00 2001 From: Antony Male Date: Sat, 4 Jan 2025 13:47:25 +0000 Subject: [PATCH 10/10] Temporarily disable mypy pre-commit checks --- .pre-commit-config.yaml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 522dcee7..7da91946 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,14 +23,16 @@ repos: rev: v0.0.275 hooks: - id: ruff - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 - hooks: - - id: mypy - # These are duplicated from requirements.txt - additional_dependencies: - [ - homeassistant-stubs==2024.12.5, - types-python-slugify==8.0.0.2, - voluptuous-stubs==0.1.1, - ] +# Temporarily disabled due to crash, probably caused by us having to use HA stubs which are out of date? +# See https://github.com/nathanmarlor/foxess_modbus/actions/runs/12610946302/job/35146070831?pr=720 +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v1.5.1 +# hooks: +# - id: mypy +# # These are duplicated from requirements.txt +# additional_dependencies: +# [ +# homeassistant-stubs==2024.12.5, +# types-python-slugify==8.0.0.2, +# voluptuous-stubs==0.1.1, +# ]