Skip to content

Commit

Permalink
Merge pull request #720 from canton7/bugfix/ha-2025.1.0
Browse files Browse the repository at this point in the history
Fix breakage caused by HA 2025.1.0
  • Loading branch information
canton7 authored Jan 4, 2025
2 parents a1f7eea + 7d656f2 commit 464d02f
Show file tree
Hide file tree
Showing 12 changed files with 65 additions and 112 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye",
"image": "mcr.microsoft.com/vscode/devcontainers/python:3.13-bullseye",
"name": "Foxess Modbus Container",
"appPort": ["9123:8123"],
"postCreateCommand": ".devcontainer/setup",
Expand Down
3 changes: 0 additions & 3 deletions .devcontainer/setup
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
5 changes: 1 addition & 4 deletions .github/workflows/tests.yaml → .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
- cron: "0 0 * * *"

env:
DEFAULT_PYTHON: "3.11"
DEFAULT_PYTHON: "3.13"

jobs:
pre-commit:
Expand All @@ -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: |
Expand Down
24 changes: 13 additions & 11 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ repos:
rev: v0.0.275
hooks:
- id: ruff
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.4.1
hooks:
- id: mypy
# These are duplicated from requirements.txt
additional_dependencies:
[
homeassistant-stubs==2023.7.0,
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,
# ]
31 changes: 6 additions & 25 deletions custom_components/foxess_modbus/client/custom_modbus_tcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
35 changes: 17 additions & 18 deletions custom_components/foxess_modbus/client/modbus_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
},
}

Expand All @@ -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
Expand Down Expand Up @@ -199,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)
Expand All @@ -226,7 +225,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
Expand Down
27 changes: 11 additions & 16 deletions custom_components/foxess_modbus/entities/entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 4 additions & 11 deletions custom_components/foxess_modbus/entities/modbus_entity_mixin.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Sensor"""

import inspect
import logging
from dataclasses import dataclass
from datetime import timedelta
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion custom_components/foxess_modbus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion hacs.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "FoxESS - Modbus",
"hacs": "1.20.0",
"homeassistant": "2023.7.0",
"homeassistant": "2025.1.0",
"render_readme": true
}
15 changes: 8 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
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

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
homeassistant-stubs==2023.7.0 # Matching HA version
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
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

0 comments on commit 464d02f

Please sign in to comment.