Skip to content

Commit 464d02f

Browse files
authored
Merge pull request #720 from canton7/bugfix/ha-2025.1.0
Fix breakage caused by HA 2025.1.0
2 parents a1f7eea + 7d656f2 commit 464d02f

File tree

12 files changed

+65
-112
lines changed

12 files changed

+65
-112
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye",
2+
"image": "mcr.microsoft.com/vscode/devcontainers/python:3.13-bullseye",
33
"name": "Foxess Modbus Container",
44
"appPort": ["9123:8123"],
55
"postCreateCommand": ".devcontainer/setup",

.devcontainer/setup

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,5 @@ set -e
55
cd "$(dirname "$0")/.."
66

77
python3 -m pip install -r requirements.txt
8-
# json files on syrupy are broken before 4.0.4, but pytest-homeassistant-custom-component for our current HA version
9-
# relies on 4.0.2
10-
python3 -m pip install --no-deps syrupy==4.0.4
118

129
git config --global --fixed-value --replace-all safe.directory "${PWD}" "${PWD}"

.github/workflows/tests.yaml renamed to .github/workflows/tests.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
- cron: "0 0 * * *"
1212

1313
env:
14-
DEFAULT_PYTHON: "3.11"
14+
DEFAULT_PYTHON: "3.13"
1515

1616
jobs:
1717
pre-commit:
@@ -29,9 +29,6 @@ jobs:
2929
- name: Install Python modules
3030
run: |
3131
pip install --no-cache-dir -r requirements.txt
32-
# json files on syrupy are broken before 4.0.4, but pytest-homeassistant-custom-component for our current HA
33-
# version relies on 4.0.2
34-
pip install --no-deps syrupy==4.0.4
3532
3633
- name: Run pre-commit on all files
3734
run: |

.pre-commit-config.yaml

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ repos:
2323
rev: v0.0.275
2424
hooks:
2525
- id: ruff
26-
- repo: https://github.com/pre-commit/mirrors-mypy
27-
rev: v1.4.1
28-
hooks:
29-
- id: mypy
30-
# These are duplicated from requirements.txt
31-
additional_dependencies:
32-
[
33-
homeassistant-stubs==2023.7.0,
34-
types-python-slugify==8.0.0.2,
35-
voluptuous-stubs==0.1.1,
36-
]
26+
# Temporarily disabled due to crash, probably caused by us having to use HA stubs which are out of date?
27+
# See https://github.com/nathanmarlor/foxess_modbus/actions/runs/12610946302/job/35146070831?pr=720
28+
# - repo: https://github.com/pre-commit/mirrors-mypy
29+
# rev: v1.5.1
30+
# hooks:
31+
# - id: mypy
32+
# # These are duplicated from requirements.txt
33+
# additional_dependencies:
34+
# [
35+
# homeassistant-stubs==2024.12.5,
36+
# types-python-slugify==8.0.0.2,
37+
# voluptuous-stubs==0.1.1,
38+
# ]

custom_components/foxess_modbus/client/custom_modbus_tcp_client.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
class CustomModbusTcpClient(ModbusTcpClient):
1515
"""Custom ModbusTcpClient subclass with some hacks"""
1616

17-
def __init__(self, delay_on_connect: int | None, **kwargs: Any) -> None:
17+
def __init__(self, delay_on_connect: int | None = None, **kwargs: Any) -> None:
1818
super().__init__(**kwargs)
1919
self._delay_on_connect = delay_on_connect
2020

2121
def connect(self) -> bool:
2222
was_connected = self.socket is not None
2323
if not was_connected:
24-
_LOGGER.debug("Connecting to %s", self.params)
24+
_LOGGER.debug("Connecting to %s", self.comm_params)
2525
is_connected = cast(bool, super().connect())
2626
# pymodbus doesn't disable Nagle's algorithm. This slows down reads quite substantially as the
2727
# TCP stack waits to see if we're going to send anything else. Disable it ourselves.
@@ -34,7 +34,7 @@ def connect(self) -> bool:
3434

3535
# Replacement of ModbusTcpClient to use poll rather than select, see
3636
# https://github.com/nathanmarlor/foxess_modbus/issues/275
37-
def recv(self, size: int) -> bytes:
37+
def recv(self, size: int | None) -> bytes:
3838
"""Read data from the underlying descriptor."""
3939
super(ModbusTcpClient, self).recv(size)
4040
if not self.socket:
@@ -48,13 +48,9 @@ def recv(self, size: int) -> bytes:
4848
# is received or timeout is expired.
4949
# If timeout expires returns the read data, also if its length is
5050
# less than the expected size.
51-
self.socket.setblocking(0)
51+
self.socket.setblocking(False)
5252

53-
# In the base method this is 'timeout = self.comm_params.timeout', but that changed from 'self.params.timeout'
54-
# in 3.4.1. So we don't have a consistent way to access the timeout.
55-
# However, this just mirrors what we set, which is the default of 3s. So use that.
56-
# Annoyingly 3.4.1
57-
timeout = 3
53+
timeout = self.comm_params.timeout_connect or 0
5854

5955
# If size isn't specified read up to 4096 bytes at a time.
6056
if size is None:
@@ -94,20 +90,5 @@ def recv(self, size: int) -> bytes:
9490
if time_ > end:
9591
break
9692

93+
self.last_frame_end = round(time.time(), 6)
9794
return b"".join(data)
98-
99-
# Replacement of ModbusTcpClient to use poll rather than select, see
100-
# https://github.com/nathanmarlor/foxess_modbus/issues/275
101-
def _check_read_buffer(self) -> bytes | None:
102-
"""Check read buffer."""
103-
time_ = time.time()
104-
end = time_ + self.params.timeout
105-
data = None
106-
107-
assert self.socket is not None
108-
poll = select.poll()
109-
poll.register(self.socket, select.POLLIN)
110-
poll_res = poll.poll(end - time_)
111-
if len(poll_res) > 0:
112-
data = self.socket.recv(1024)
113-
return data

custom_components/foxess_modbus/client/modbus_client.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@
1313
from homeassistant.core import HomeAssistant
1414
from pymodbus.client import ModbusSerialClient
1515
from pymodbus.client import ModbusUdpClient
16-
from pymodbus.pdu import ModbusResponse
17-
from pymodbus.register_read_message import ReadHoldingRegistersResponse
18-
from pymodbus.register_read_message import ReadInputRegistersResponse
19-
from pymodbus.register_write_message import WriteMultipleRegistersResponse
20-
from pymodbus.register_write_message import WriteSingleRegisterResponse
21-
from pymodbus.transaction import ModbusRtuFramer
22-
from pymodbus.transaction import ModbusSocketFramer
16+
from pymodbus.framer import FramerType
17+
from pymodbus.pdu import ModbusPDU
18+
from pymodbus.pdu.register_read_message import ReadHoldingRegistersResponse
19+
from pymodbus.pdu.register_read_message import ReadInputRegistersResponse
20+
from pymodbus.pdu.register_write_message import WriteMultipleRegistersResponse
21+
from pymodbus.pdu.register_write_message import WriteSingleRegisterResponse
2322

2423
from .. import client
2524
from ..common.types import ConnectionType
@@ -39,19 +38,19 @@
3938
_CLIENTS: dict[str, dict[str, Any]] = {
4039
SERIAL: {
4140
"client": ModbusSerialClient,
42-
"framer": ModbusRtuFramer,
41+
"framer": FramerType.RTU,
4342
},
4443
TCP: {
4544
"client": CustomModbusTcpClient,
46-
"framer": ModbusSocketFramer,
45+
"framer": FramerType.SOCKET,
4746
},
4847
UDP: {
4948
"client": ModbusUdpClient,
50-
"framer": ModbusSocketFramer,
49+
"framer": FramerType.SOCKET,
5150
},
5251
RTU_OVER_TCP: {
5352
"client": CustomModbusTcpClient,
54-
"framer": ModbusRtuFramer,
53+
"framer": FramerType.RTU,
5554
},
5655
}
5756

@@ -70,14 +69,16 @@ def __init__(self, hass: HomeAssistant, protocol: str, adapter: InverterAdapter,
7069

7170
client = _CLIENTS[protocol]
7271

73-
# Delaying for a second after establishing a connection seems to help the inverter stability,
74-
# see https://github.com/nathanmarlor/foxess_modbus/discussions/132
7572
config = {
7673
**config,
7774
"framer": client["framer"],
78-
"delay_on_connect": 1 if adapter.connection_type == ConnectionType.LAN else None,
7975
}
8076

77+
# Delaying for a second after establishing a connection seems to help the inverter stability,
78+
# see https://github.com/nathanmarlor/foxess_modbus/discussions/132
79+
if adapter.connection_type == ConnectionType.LAN:
80+
config["delay_on_connect"] = 1
81+
8182
# If our custom PosixPollSerial hack is supported, use that. This uses poll rather than select, which means we
8283
# don't break when there are more than 1024 fds. See #457.
8384
# Only supported on posix, see https://github.com/pyserial/pyserial/blob/7aeea35429d15f3eefed10bbb659674638903e3a/serial/__init__.py#L31
@@ -199,12 +200,10 @@ async def _async_pymodbus_call(self, call: Callable[..., T], *args: Any, auto_co
199200
"""Convert async to sync pymodbus call."""
200201

201202
def _call() -> T:
202-
# pymodbus 3.4.1 removes automatic reconnections for the sync modbus client.
203-
# However, in versions prior to 4.3.0, the ModbusUdpClient didn't have a connected property.
204203
# When using pollserial://, connected calls into serial.serial_for_url, which calls importlib.import_module,
205204
# which HA doesn't like (see https://github.com/nathanmarlor/foxess_modbus/issues/618).
206205
# Therefore we need to do this check inside the executor job
207-
if auto_connect and hasattr(self._client, "connected") and not self._client.connected:
206+
if auto_connect and not self._client.connected:
208207
self._client.connect()
209208
# If the connection failed, this call will throw an appropriate error
210209
return call(*args)
@@ -226,7 +225,7 @@ def __str__(self) -> str:
226225
class ModbusClientFailedError(Exception):
227226
"""Raised when the ModbusClient fails to read/write"""
228227

229-
def __init__(self, message: str, client: ModbusClient, response: ModbusResponse | Exception) -> None:
228+
def __init__(self, message: str, client: ModbusClient, response: ModbusPDU | Exception) -> None:
230229
super().__init__(f"{message} from {client}: {response}")
231230
self.message = message
232231
self.client = client

custom_components/foxess_modbus/entities/entity_factory.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,22 @@
1313
from ..common.types import RegisterType
1414
from .inverter_model_spec import InverterModelSpec
1515

16+
1617
# HA introduced a FrozenOrThawed metaclass which is used by EntityDescription.
1718
# This conflicts with ABC's metaclass.
18-
# If EntityDescription has a metaclass (FrozenOrThawed), we need to combine that with
19-
# ABC's metaclass, see https://github.com/nathanmarlor/foxess_modbus/issues/480.
20-
# This is to allow HA to move to frozen entity descriptions (to aid caching), and will
21-
# start logging deprecation warnings in 2024.x.
22-
if type(EntityDescription) == type(type): # type: ignore
23-
_METACLASS = type(ABC)
24-
ENTITY_DESCRIPTION_KWARGS = {}
25-
else:
26-
27-
class EntityFactoryMetaclass(type(EntityDescription), type(ABC)): # type: ignore
28-
"""
29-
Metaclass to use for EntityFactory.
30-
"""
19+
# We need to combine EntityDescription's metaclass with ABC's metaclass, see
20+
# https://github.com/nathanmarlor/foxess_modbus/issues/480. This is to allow HA to move to frozen entity descriptions
21+
# (to aid caching), and will start logging deprecation warnings in 2024.x.
22+
class EntityFactoryMetaclass(type(EntityDescription), type(ABC)): # type: ignore
23+
"""
24+
Metaclass to use for EntityFactory.
25+
"""
26+
3127

32-
_METACLASS = EntityFactoryMetaclass
33-
ENTITY_DESCRIPTION_KWARGS = {"frozen": True}
28+
ENTITY_DESCRIPTION_KWARGS = {"frozen": True}
3429

3530

36-
class EntityFactory(ABC, metaclass=_METACLASS): # type: ignore
31+
class EntityFactory(ABC, metaclass=EntityFactoryMetaclass): # type: ignore
3732
"""Factory which can create entities"""
3833

3934
@property

custom_components/foxess_modbus/entities/modbus_entity_mixin.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Mixin providing common functionality for all entity classes"""
22

33
import logging
4-
from abc import ABC
54
from typing import TYPE_CHECKING
65
from typing import Any
76
from typing import Protocol
@@ -73,21 +72,15 @@ class ModbusEntityProtocol(Protocol):
7372
else:
7473
_ModbusEntityMixinBase = object
7574

75+
7676
# HA introduced a ABCCachedProperties metaclass which is used by Entity, and which derives from ABCMeta.
7777
# This conflicts with Protocol's metaclass (from ModbusEntityProtocol).
78-
if type(Entity) == type(ABC):
79-
_METACLASS = type(Entity)
80-
81-
else:
82-
83-
class ModbusEntityMixinMetaclass(type(Entity), type(Protocol)): # type: ignore
84-
pass
85-
86-
_METACLASS = ModbusEntityMixinMetaclass
78+
class ModbusEntityMixinMetaclass(type(Entity), type(Protocol)): # type: ignore
79+
pass
8780

8881

8982
class ModbusEntityMixin(
90-
ModbusControllerEntity, ModbusEntityProtocol, _ModbusEntityMixinBase, metaclass=_METACLASS # type: ignore
83+
ModbusControllerEntity, ModbusEntityProtocol, _ModbusEntityMixinBase, metaclass=ModbusEntityMixinMetaclass
9184
):
9285
"""
9386
Mixin for subclasses of Entity

custom_components/foxess_modbus/entities/modbus_integration_sensor.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Sensor"""
22

3-
import inspect
43
import logging
54
from dataclasses import dataclass
65
from datetime import timedelta
@@ -26,18 +25,7 @@
2625

2726
_LOGGER = logging.getLogger(__name__)
2827

29-
30-
def _make_integration_sensor_kwargs() -> dict[str, Any]:
31-
# HA 2024.7 introduced a new non-optional max_sub_interval parameter
32-
kwargs: dict[str, Any] = {}
33-
args = inspect.signature(IntegrationSensor.__init__)
34-
if "max_sub_interval" in args.parameters:
35-
kwargs["max_sub_interval"] = timedelta(minutes=1) # Default used by integration sensor config
36-
37-
return kwargs
38-
39-
40-
_INTEGRATION_SENSOR_KWARGS = _make_integration_sensor_kwargs()
28+
MAX_SUB_INTERVAL = timedelta(minutes=1) # Default used by integration sensor config
4129

4230

4331
@dataclass(kw_only=True, **ENTITY_DESCRIPTION_KWARGS)
@@ -121,7 +109,7 @@ def __init__(
121109
unique_id=None,
122110
unit_prefix=None,
123111
unit_time=unit_time,
124-
**_INTEGRATION_SENSOR_KWARGS,
112+
max_sub_interval=MAX_SUB_INTERVAL,
125113
)
126114

127115
# Use the icon from entity_description

custom_components/foxess_modbus/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
"integration_type": "service",
99
"iot_class": "local_push",
1010
"issue_tracker": "https://github.com/nathanmarlor/foxess_modbus/issues",
11-
"requirements": ["pymodbus>=3.1.3"],
11+
"requirements": ["pymodbus>=3.7.4"],
1212
"version": "1.0.0"
1313
}

hacs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "FoxESS - Modbus",
33
"hacs": "1.20.0",
4-
"homeassistant": "2023.7.0",
4+
"homeassistant": "2025.1.0",
55
"render_readme": true
66
}

requirements.txt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
pip>=21.0,<23.2
2-
homeassistant==2023.7.0
1+
# pip>=21.0,<23.2
2+
homeassistant==2025.1.0
33

44
# Testing
5-
pytest-homeassistant-custom-component==0.13.42 # Matching version for 2023.7.0
5+
pytest-homeassistant-custom-component==0.13.201 # Matching version for 2025.1.0
66
psutil-home-assistant # Not sure why this is needed?
77
fnv_hash_fast # Or this?
88
pytest-asyncio
99

1010
colorlog==6.7.0
1111
pre-commit==3.3.3
12-
black==23.3.0
12+
black==23.9.0
1313
ruff==0.0.275
1414
# These are duplicated in .pre-commit-config.yaml
1515
reorder-python-imports==3.10.0
16-
mypy==1.4.1
17-
homeassistant-stubs==2023.7.0 # Matching HA version
16+
mypy==1.5.1
17+
# Currently not avaiable, see https://github.com/KapJI/homeassistant-stubs/issues/510
18+
# homeassistant-stubs==2025.1.0 # Matching HA version
1819
types-python-slugify==8.0.0.2
1920
voluptuous-stubs==0.1.1
2021
# For mypy. Keep in sync with manifest.json and https://github.com/home-assistant/core/blob/master/requirements_all.txt.
2122
# If changed, make sure subclasses in modbus_client are still valid!
22-
pymodbus==3.5.4
23+
pymodbus==3.7.4
2324
pyserial==3.5

0 commit comments

Comments
 (0)