Skip to content

Commit 12dc26d

Browse files
authored
Add files via upload
1 parent 706bedf commit 12dc26d

4 files changed

Lines changed: 86 additions & 60 deletions

File tree

custom_components/solax_modbus/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,22 +1584,23 @@ def treat_address(self, data: dict[str, Any], regs: list[int], idx: int, descr:
15841584
return_value = round(val * descr.scale * read_scale, descr.rounding)
15851585
except Exception:
15861586
return_value = val # probably a REGISTER_WORDS instance
1587-
if descr.native_unit_of_measurement == UnitOfFrequency.HERTZ:
1587+
native_unit = getattr(descr, "native_unit_of_measurement", None)
1588+
if native_unit == UnitOfFrequency.HERTZ:
15881589
min_val = getattr(descr, "min_value", 20)
15891590
max_val = getattr(descr, "max_value", 80)
1590-
if descr.native_unit_of_measurement == PERCENTAGE:
1591+
if native_unit == PERCENTAGE:
15911592
min_val = getattr(descr, "min_value", 0)
15921593
max_val = getattr(descr, "max_value", 100)
1593-
elif descr.native_unit_of_measurement == UnitOfTemperature.CELSIUS:
1594+
elif native_unit == UnitOfTemperature.CELSIUS:
15941595
min_val = getattr(descr, "min_value", -100)
15951596
max_val = getattr(descr, "max_value", 200)
1596-
elif descr.native_unit_of_measurement == UnitOfPower.KILO_WATT:
1597+
elif native_unit == UnitOfPower.KILO_WATT:
15971598
min_val = getattr(descr, "min_value", -self.inverterPowerKw * 2)
15981599
max_val = getattr(descr, "max_value", +self.inverterPowerKw * 2)
1599-
elif descr.native_unit_of_measurement == UnitOfElectricCurrent.AMPERE:
1600+
elif native_unit == UnitOfElectricCurrent.AMPERE:
16001601
min_val = getattr(descr, "min_value", -self.inverterPowerKw * 2)
16011602
max_val = getattr(descr, "max_value", +self.inverterPowerKw * 2)
1602-
elif descr.native_unit_of_measurement == UnitOfElectricPotential.VOLT:
1603+
elif native_unit == UnitOfElectricPotential.VOLT:
16031604
min_val = getattr(descr, "min_value", 0)
16041605
max_val = getattr(descr, "max_value", 2000)
16051606
else:

custom_components/solax_modbus/const.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,14 +307,26 @@ class BaseModbusTimeEntityDescription(TimeEntityDescription):
307307
allowedtypes: int = 0 # overload with ALLDEFAULT from plugin
308308
modbus_min: int | None = None # Minimum supported Modbus protocol document version, e.g. 102 for V001.02.
309309
modbus_max: int | None = None # Maximum supported Modbus protocol document version.
310-
register: int | None = None
310+
scale: float | dict[Any, Any] | Callable[[Any, Any, dict[str, Any]], Any] = 1
311+
read_scale_exceptions: list[Any] | None = None
312+
read_scale: float = 1
313+
register: int = -1
314+
rounding: int = 1
315+
register_type: int | None = None # REG_HOLDING or REG_INPUT or REG_DATA
316+
register_data_type: str | None = REGISTER_U16 # REGISTER_U16, REGISTER_S32, REGISTER_F32, etc.
317+
scan_group: str | None = None # SCAN_GROUP_MEDIUM, SCAN_GROUP_FAST, SCAN_GROUP_DEFAULT, etc.
318+
newblock: bool = False # set to True to start a new modbus read block operation
311319
option_dict: dict[int, str] | None = None
312320
reverse_option_dict: dict[str, int] | None = None # autocomputed
313321
blacklist: list[str] | None = None # none or list of serial number prefixes
314322
write_method: int = WRITE_SINGLE_MODBUS # WRITE_SINGLE_MOBUS or WRITE_MULTI_MODBUS or WRITE_DATA_LOCAL
315323
initvalue: int | None = None # initial default value for WRITE_DATA_LOCAL entities
316-
register_data_type: str | None = None # REGISTER_U16, REGISTER_S32, REGISTER_F32, etc.
317324
wordcount: int | None = None # number of registers to write (for separate register format, e.g., hours and minutes in adjacent registers)
325+
sleepmode: int | None = SLEEPMODE_LAST # or SLEEPMODE_ZERO, SLEEPMODE_NONE or SLEEPMODE_LASTAWAKE
326+
ignore_readerror: bool | Any = False
327+
min_value: int | None = None
328+
max_value: int | None = None
329+
depends_on: list[str] | None = None # list of modbus register keys that must be read
318330

319331

320332
@dataclass(kw_only=True, frozen=True)

custom_components/solax_modbus/plugin_solax.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from collections.abc import Sequence
3-
from dataclasses import dataclass, replace
3+
from dataclasses import dataclass, fields, replace
44
from time import time
55
from typing import Any
66

@@ -11288,6 +11288,39 @@ def value_function_battery_voltage_cell_difference(initval: int, descr: Any, dat
1128811288
),
1128911289
]
1129011290

11291+
11292+
_TIME_OPTION_DICTS = (TIME_OPTIONS, TIME_OPTIONS_GEN4, TIME_OPTIONS_SEPARATE_REGISTERS)
11293+
_TIME_ENTITY_FIELD_NAMES = {field.name for field in fields(SolaXModbusTimeEntityDescription)}
11294+
_TIME_ENTITY_KEYS = {(time_entity.key, time_entity.register, time_entity.allowedtypes) for time_entity in TIME_TYPES}
11295+
11296+
11297+
def _is_time_select(select_entity: SolaxModbusSelectEntityDescription) -> bool:
11298+
"""Return True for legacy time selects that should be exposed as TimeEntity."""
11299+
return any(select_entity.option_dict is option_dict for option_dict in _TIME_OPTION_DICTS)
11300+
11301+
11302+
def _time_entity_from_select(select_entity: SolaxModbusSelectEntityDescription) -> SolaXModbusTimeEntityDescription:
11303+
"""Convert a legacy time select descriptor into a native HA TimeEntity descriptor."""
11304+
return SolaXModbusTimeEntityDescription(
11305+
**{
11306+
field_name: getattr(select_entity, field_name)
11307+
for field_name in _TIME_ENTITY_FIELD_NAMES
11308+
if hasattr(select_entity, field_name)
11309+
}
11310+
)
11311+
11312+
11313+
for _select_entity in SELECT_TYPES:
11314+
if not _is_time_select(_select_entity):
11315+
continue
11316+
_time_key = (_select_entity.key, _select_entity.register, _select_entity.allowedtypes)
11317+
if _time_key in _TIME_ENTITY_KEYS:
11318+
continue
11319+
TIME_TYPES.append(_time_entity_from_select(_select_entity))
11320+
_TIME_ENTITY_KEYS.add(_time_key)
11321+
11322+
SELECT_TYPES = [select_entity for select_entity in SELECT_TYPES if not _is_time_select(select_entity)]
11323+
1129111324
# ============================ plugin declaration =================================================
1129211325

1129311326

custom_components/solax_modbus/time.py

Lines changed: 31 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,24 @@ def _handle_local_data_loaded(self, event: Any) -> None:
115115
return
116116
self.modbus_data_updated()
117117

118+
def _parse_time_string(self, time_val: str) -> datetime_time | None:
119+
"""Parse common time string formats into a datetime.time object."""
120+
time_val = time_val.strip()
121+
if not time_val:
122+
_LOGGER.debug(f"{self._platform_name}: empty time string for {self._key}")
123+
return None
124+
125+
for fmt in ["%H:%M", "%H:%M:%S", "%H:%M:%S.%f"]:
126+
try:
127+
parsed = datetime.strptime(time_val, fmt)
128+
_LOGGER.debug(f"{self._platform_name}: parsed {self._key} as {fmt}: {parsed.time()}")
129+
return parsed.time()
130+
except ValueError:
131+
continue
132+
133+
_LOGGER.debug(f"{self._platform_name}: unrecognized time format for {self._key}: {time_val}")
134+
return None
135+
118136
def _parse_time_value(self) -> datetime_time | None:
119137
"""Parse the time value from hub.data and return a datetime.time object.
120138
@@ -136,57 +154,17 @@ def _parse_time_value(self) -> datetime_time | None:
136154

137155
# Handle string time values in hh:mm format
138156
if isinstance(time_val, str):
139-
# Strip whitespace and handle empty strings
140-
time_val = time_val.strip()
141-
if not time_val:
142-
_LOGGER.debug(f"{self._platform_name}: empty time string for {self._key}")
143-
return None
144-
# Common time formats
145-
for fmt in ["%H:%M", "%H:%M:%S", "%H:%M:%S.%f"]:
146-
try:
147-
parsed = datetime.strptime(time_val, fmt)
148-
_LOGGER.debug(f"{self._platform_name}: parsed {self._key} as {fmt}: {parsed.time()}")
149-
return parsed.time()
150-
except ValueError:
151-
continue
152-
# Try parsing as HH:MM:SS with seconds (8 chars like 05:25:30)
153-
if len(time_val) == 8 and time_val[2] == ":" and time_val[5] == ":":
154-
try:
155-
parsed = datetime.strptime(time_val, "%H:%M:%S")
156-
_LOGGER.debug(f"{self._platform_name}: parsed {self._key} as HH:MM:SS: {parsed.time()}")
157-
return parsed.time()
158-
except ValueError:
159-
pass
160-
# Try parsing as HH:MM (5 chars like 05:25)
161-
if len(time_val) == 5 and time_val[2] == ":":
162-
try:
163-
parsed = datetime.strptime(time_val, "%H:%M")
164-
_LOGGER.debug(f"{self._platform_name}: parsed {self._key} as HH:MM: {parsed.time()}")
165-
return parsed.time()
166-
except ValueError:
167-
pass
168-
# If we get here, the string format was not recognized
169-
_LOGGER.debug(f"{self._platform_name}: unrecognized time format for {self._key}: {time_val}")
170-
return None
157+
return self._parse_time_string(time_val)
171158

172-
# Handle numeric values (e.g., from value_function_gen4time or value_function_gen23time)
159+
# Handle raw Modbus payloads by translating through the descriptor's option table.
173160
if isinstance(time_val, (int, float)):
174-
# Try to convert to string and parse
175-
time_str = str(time_val)
176-
if len(time_str) == 5 and time_str[2] == ":":
177-
try:
178-
parsed = datetime.strptime(time_str, "%H:%M")
179-
_LOGGER.debug(f"{self._platform_name}: parsed numeric {self._key} as HH:MM: {parsed.time()}")
180-
return parsed.time()
181-
except ValueError:
182-
pass
183-
if len(time_str) == 8 and time_str[2] == ":" and time_str[5] == ":":
184-
try:
185-
parsed = datetime.strptime(time_str, "%H:%M:%S")
186-
_LOGGER.debug(f"{self._platform_name}: parsed numeric {self._key} as HH:MM:SS: {parsed.time()}")
187-
return parsed.time()
188-
except ValueError:
189-
pass
161+
payload = int(time_val)
162+
if self._option_dict is not None:
163+
mapped_value = self._option_dict.get(payload)
164+
if mapped_value is not None:
165+
return self._parse_time_string(mapped_value)
166+
_LOGGER.debug(f"{self._platform_name}: no time option mapping for {self._key} payload {payload}")
167+
return None
190168

191169
_LOGGER.debug(f"{self._platform_name}: time value for {self._key} is not a string or datetime: {type(time_val)}")
192170
return None
@@ -225,7 +203,7 @@ async def async_set_value(self, value: datetime_time | None) -> None:
225203

226204
# Find the corresponding payload from option_dict
227205
payload = None
228-
for key, time_val in self._option_dict.items():
206+
for key, time_val in (self._option_dict or {}).items():
229207
if time_val == time_str:
230208
payload = key
231209
break
@@ -259,7 +237,9 @@ async def async_set_value(self, value: datetime_time | None) -> None:
259237
elif self._write_method == WRITE_DATA_LOCAL:
260238
_LOGGER.info(f"*** local data written {self._key}: {time_str}")
261239
self._hub.localsUpdated = True # mark to save permanently
262-
self._hub.data[self._key] = time_str
240+
241+
self._hub.data[self._key] = time_str
242+
self._attr_native_value = value
263243

264244
self.async_write_ha_state()
265245

0 commit comments

Comments
 (0)