Skip to content

Commit

Permalink
Merge pull request #623 from canton7/bugfix/h3-pro-fixes
Browse files Browse the repository at this point in the history
More H3-Pro fixes
  • Loading branch information
canton7 authored May 29, 2024
2 parents 99da96d + 2306f6f commit e8aa293
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,5 @@ async def write_registers(self, start_address: int, values: list[int]) -> None:
"""Write multiple registers"""

@abstractmethod
def read(self, address: int, *, signed: bool) -> int | None:
def read(self, address: int | list[int], *, signed: bool) -> int | None:
"""Fetch the last-read value for the given address, or None if none is avaiable"""
12 changes: 5 additions & 7 deletions custom_components/foxess_modbus/entities/entity_descriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1036,8 +1036,8 @@ def _ct2_meter(phase: str | None, scale: float, addresses: list[ModbusAddressesS
yield _ct2_meter("T", scale=0.0001, addresses=[ModbusAddressesSpec(holding=[38921, 38920], models=Inv.H3_PRO)])

def _load_power(phase: str | None, *, addresses: list[ModbusAddressesSpec]) -> EntityFactory:
key_suffix = f"_{phase}" if phase is not None else None
name_suffix = f" {phase}" if phase is not None else None
key_suffix = f"_{phase}" if phase is not None else ""
name_suffix = f" {phase}" if phase is not None else ""
return ModbusSensorDescription(
key=f"load_power{key_suffix}",
addresses=addresses,
Expand Down Expand Up @@ -2022,12 +2022,10 @@ def _configuration_entities() -> Iterable[EntityFactory]:
],
name="Work Mode",
options_map={
0: "Self Use",
1: "Feed-in First",
2: "Back-up",
1: "Self Use",
2: "Feed-in First",
3: "Back-up",
4: "Peak Shaving",
6: "Force Charge",
7: "Force Discharge",
},
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from dataclasses import dataclass
from enum import Enum
from enum import auto
from typing import Callable

from homeassistant.components.number import NumberDeviceClass
Expand All @@ -16,6 +18,12 @@
from .modbus_remote_control_select import ModbusRemoteControlSelectDescription


class WorkMode(Enum):
SELF_USE = auto()
FEED_IN_FIRST = auto()
BACK_UP = auto()


@dataclass(frozen=True)
class ModbusRemoteControlAddressConfig:
"""Defines the set of registers used for remote control"""
Expand All @@ -28,14 +36,16 @@ class ModbusRemoteControlAddressConfig:
"""Remote control-Active power command, sets the output power (+ve) or input power (-ve) of the inverter"""
work_mode: int | None
"""Work mode control"""
work_mode_map: dict[WorkMode, int] | None
"""Map of work mode ->value"""

battery_soc: int
"""Current battery SoC"""
max_soc: int | None
"""Configured Max SoC"""
invbatpower: int
invbatpower: list[int]
"""Current battery charge (negative) / discharge (positive) power"""
pwr_limit_bat_up: int | None
pwr_limit_bat_up: list[int] | None
"""Prw_limit Bat_up, maximum power that the battery can accept"""
pv_voltages: list[int]
"""Array of pvx_voltage addresses for PV strings"""
Expand Down
13 changes: 3 additions & 10 deletions custom_components/foxess_modbus/entities/modbus_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,11 @@ def __init__(

def _calculate_native_value(self) -> int | float | None:
"""Return the value reported by the sensor."""
original = 0
for i, address in enumerate(self._addresses):
register_value = self._controller.read(address, signed=False)
if register_value is None:
return None
original |= (register_value & 0xFFFF) << (i * 16)

entity_description = cast(ModbusSensorDescription, self.entity_description)
original = self._controller.read(self._addresses, signed=entity_description.signed)

if entity_description.signed:
sign_bit = 1 << (len(self._addresses) * 16 - 1)
original = (original & (sign_bit - 1)) - (original & sign_bit)
if original is None:
return None

value: float | int = original

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
from .modbus_remote_control_config import ModbusRemoteControlAddressConfig
from .modbus_remote_control_config import ModbusRemoteControlFactory
from .modbus_remote_control_config import RemoteControlAddressSpec
from .modbus_remote_control_config import WorkMode

_NORMAL_WORK_MODE_MAP = {
WorkMode.SELF_USE: 0,
WorkMode.FEED_IN_FIRST: 1,
WorkMode.BACK_UP: 2,
}

REMOTE_CONTROL_DESCRIPTION = ModbusRemoteControlFactory(
addresses=[
Expand All @@ -11,10 +18,11 @@
timeout_set=44001,
active_power=[44002],
work_mode=41000,
work_mode_map=_NORMAL_WORK_MODE_MAP,
max_soc=41010,
invbatpower=11008,
invbatpower=[11008],
battery_soc=11036,
pwr_limit_bat_up=44012,
pwr_limit_bat_up=[44012],
pv_voltages=[11000, 11003],
),
models=Inv.H1_G1,
Expand All @@ -26,8 +34,9 @@
timeout_set=44001,
active_power=[44002],
work_mode=None,
work_mode_map=None,
max_soc=None,
invbatpower=31022,
invbatpower=[31022],
battery_soc=31024,
pwr_limit_bat_up=None,
pv_voltages=[31000, 31003],
Expand All @@ -40,10 +49,11 @@
timeout_set=44001,
active_power=[44002],
work_mode=41000,
work_mode_map=_NORMAL_WORK_MODE_MAP,
max_soc=41010,
invbatpower=31022,
invbatpower=[31022],
battery_soc=31024,
pwr_limit_bat_up=44012,
pwr_limit_bat_up=[44012],
pv_voltages=[39070, 39072],
),
models=Inv.H1_G2,
Expand All @@ -55,8 +65,9 @@
timeout_set=44001,
active_power=[44002],
work_mode=41000,
work_mode_map=_NORMAL_WORK_MODE_MAP,
max_soc=41010,
invbatpower=11008,
invbatpower=[11008],
battery_soc=11036,
pwr_limit_bat_up=None,
pv_voltages=[11000, 11003, 11096, 11099],
Expand All @@ -69,8 +80,9 @@
timeout_set=44001,
active_power=[44002],
work_mode=41000,
work_mode_map=_NORMAL_WORK_MODE_MAP,
max_soc=41010,
invbatpower=31022,
invbatpower=[31022],
battery_soc=31024,
pwr_limit_bat_up=None,
pv_voltages=[31000, 31003, 31039, 31042],
Expand All @@ -85,14 +97,35 @@
timeout_set=44001,
active_power=[44003, 44002],
work_mode=41000,
work_mode_map=_NORMAL_WORK_MODE_MAP,
max_soc=41010,
invbatpower=31022,
invbatpower=[31022],
battery_soc=31038,
pwr_limit_bat_up=None,
pv_voltages=[31000, 31003],
),
models=Inv.H3_SET & ~Inv.KUARA_H3 & ~Inv.AIO_H3,
),
# The H3 Pro has its own Force Charge / Discharge work modes
RemoteControlAddressSpec(
# The H3 doesn't support anything above 44005, and the active/reactive power regisers are 2 values
# The Kuara H3 doesn't support this, see https://github.com/nathanmarlor/foxess_modbus/issues/532
holding=ModbusRemoteControlAddressConfig(
remote_enable=46001,
timeout_set=46002,
active_power=[46004, 46003],
work_mode=49203,
work_mode_map={
WorkMode.SELF_USE: 1,
WorkMode.FEED_IN_FIRST: 2,
WorkMode.BACK_UP: 3,
},
max_soc=46610,
invbatpower=[39238, 39237],
battery_soc=31038,
pwr_limit_bat_up=[46019, 46018],
pv_voltages=[39070, 39072, 39074, 39076],
),
models=Inv.H3_PRO,
),
]
)
47 changes: 31 additions & 16 deletions custom_components/foxess_modbus/modbus_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,28 +154,43 @@ def inverter_capacity(self) -> int:
def inverter_details(self) -> dict[str, Any]:
return self._inverter_details

def read(self, address: int, *, signed: bool) -> int | None:
def read(self, address: int | list[int], *, signed: bool) -> int | None:
# There can be a delay between writing a register, and actually reading that value back (presumably the delay
# is on the inverter somewhere). If we've recently written a value, use that value, rather than the latest-read
# value
register_value = self._data.get(address)
if register_value is None:
return None

now = time.monotonic()
value: int | None
if (
register_value.written_value is not None
and register_value.written_at is not None
and now - register_value.written_at < _INVERTER_WRITE_DELAY_SECS
):
value = register_value.written_value
else:
value = register_value.read_value

if signed and value is not None:
sign_bit = 1 << (16 - 1)
def _read_value(address: int) -> int | None:
register_value = self._data.get(address)
if register_value is None:
return None

value: int | None
if (
register_value.written_value is not None
and register_value.written_at is not None
and now - register_value.written_at < _INVERTER_WRITE_DELAY_SECS
):
value = register_value.written_value
else:
value = register_value.read_value

return value

if isinstance(address, int):
address = [address]

value = 0
for i, a in enumerate(address):
val = _read_value(a)
if val is None:
return None
value |= (val & 0xFFFF) << (i * 16)

if signed:
sign_bit = 1 << (len(address) * 16 - 1)
value = (value & (sign_bit - 1)) - (value & sign_bit)

return value

async def read_registers(self, start_address: int, num_registers: int, register_type: RegisterType) -> list[int]:
Expand Down
34 changes: 17 additions & 17 deletions custom_components/foxess_modbus/remote_control_manager.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import logging
from enum import IntEnum

from .common.entity_controller import EntityController
from .common.entity_controller import EntityRemoteControlManager
from .common.entity_controller import ModbusControllerEntity
from .common.entity_controller import RemoteControlMode
from .entities.modbus_remote_control_config import ModbusRemoteControlAddressConfig
from .entities.modbus_remote_control_config import WorkMode

_LOGGER = logging.getLogger(__package__)


# Currently these are the same across all models
class WorkMode(IntEnum):
SELF_USE = 0
FEED_IN_FIRST = 1
BACK_UP = 2


# If the PV voltage is below this value, count it as no sun
_PV_VOLTAGE_THRESHOLD = 70

Expand All @@ -41,8 +33,8 @@ def __init__(
self._addresses.battery_soc,
self._addresses.work_mode,
self._addresses.max_soc,
self._addresses.invbatpower,
self._addresses.pwr_limit_bat_up,
*self._addresses.invbatpower,
*(self._addresses.pwr_limit_bat_up if self._addresses.pwr_limit_bat_up is not None else []),
*self._addresses.pv_voltages,
]
self._modbus_addresses = [x for x in modbus_addresses if x is not None]
Expand Down Expand Up @@ -270,9 +262,12 @@ async def _update_discharge(self) -> None:
async def _enable_remote_control(self, fallback_work_mode: WorkMode) -> None:
# We set a fallback work mode so that the inverter still does "roughly" the right thing if we disconnect
# (This might not be available, e.g. on H1 LAN)
assert self._addresses.work_mode_map is not None
fallback_work_mode_value = self._addresses.work_mode_map[fallback_work_mode]

current_work_mode = self._read(self._addresses.work_mode, signed=False)
if current_work_mode != fallback_work_mode and self._addresses.work_mode is not None:
await self._controller.write_register(self._addresses.work_mode, int(fallback_work_mode))
if current_work_mode != fallback_work_mode_value and self._addresses.work_mode is not None:
await self._controller.write_register(self._addresses.work_mode, fallback_work_mode_value)

if not self._remote_control_enabled:
self._remote_control_enabled = True
Expand All @@ -293,12 +288,17 @@ async def _disable_remote_control(self, work_mode: WorkMode | None = None) -> No
await self._controller.write_register(self._addresses.remote_enable, 0)

# This might not be available, e.g. on H1 LAN
if work_mode is not None and self._addresses.work_mode is not None:
if (
work_mode is not None
and self._addresses.work_mode is not None
and self._addresses.work_mode_map is not None
):
current_work_mode = self._read(self._addresses.work_mode, signed=False)
if current_work_mode != work_mode:
await self._controller.write_register(self._addresses.work_mode, int(work_mode))
work_mode_value = self._addresses.work_mode_map[work_mode]
if current_work_mode != work_mode_value:
await self._controller.write_register(self._addresses.work_mode, work_mode_value)

def _read(self, address: int | None, signed: bool) -> int | None:
def _read(self, address: list[int] | int | None, signed: bool) -> int | None:
if address is None:
return None
return self._controller.read(address, signed=signed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1124,8 +1124,8 @@
39225
]
},
"key": "load_powerNone",
"name": "Load PowerNone",
"key": "load_power",
"name": "Load Power",
"scale": 0.001,
"signed": true,
"type": "sensor"
Expand Down Expand Up @@ -1640,11 +1640,8 @@
"name": "Work Mode",
"type": "select",
"values": {
"0": "Self Use",
"1": "Feed-in First",
"2": "Back-up",
"4": "Peak Shaving",
"6": "Force Charge"
"1": "Self Use",
"2": "Feed-in First"
}
}
]

0 comments on commit e8aa293

Please sign in to comment.