Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions custom_components/givenergy_local/__init__.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
from __future__ import annotations

import voluptuous as vol
from givenergy_modbus.client import commands
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import device_registry as dr

from .const import (
CONF_MAX_BATTERIES,
CONF_PASSIVE,
CONF_SCAN_INTERVAL,
CONF_TIMEOUT_TOLERANCE,
DEFAULT_MAX_BATTERIES,
DEFAULT_PASSIVE,
DEFAULT_SCAN_INTERVAL,
DEFAULT_TIMEOUT_TOLERANCE,
DOMAIN,
PLATFORMS,
SERVICE_CALIBRATE_BATTERY_SOC,
SERVICE_REBOOT_INVERTER,
)
from .coordinator import GivEnergyUpdateCoordinator

SERVICE_DEVICE_SCHEMA = vol.Schema({vol.Required("device_id"): cv.string})


def _coordinator_for_device(
hass: HomeAssistant, device_id: str
) -> GivEnergyUpdateCoordinator | None:
device = dr.async_get(hass).async_get(device_id)
if device is None:
return None
for entry_id in device.config_entries:
coordinator = hass.data.get(DOMAIN, {}).get(entry_id)
if coordinator is not None:
return coordinator
return None


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = GivEnergyUpdateCoordinator(
hass=hass,
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
scan_interval=entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL),
max_batteries=entry.data.get(CONF_MAX_BATTERIES, DEFAULT_MAX_BATTERIES),
passive=entry.data.get(CONF_PASSIVE, DEFAULT_PASSIVE),
timeout_tolerance=entry.data.get(CONF_TIMEOUT_TOLERANCE, DEFAULT_TIMEOUT_TOLERANCE),
)
Expand All @@ -35,6 +54,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

if not hass.services.has_service(DOMAIN, SERVICE_REBOOT_INVERTER):

async def handle_reboot_inverter(call: ServiceCall) -> None:
c = _coordinator_for_device(hass, call.data["device_id"])
if c is None or c._client is None or not c._client.connected:
raise HomeAssistantError(
f"GivEnergy inverter for device {call.data['device_id']!r} "
"is not currently connected"
)
await c._client.one_shot_command(commands.set_inverter_reboot())

async def handle_calibrate_battery_soc(call: ServiceCall) -> None:
c = _coordinator_for_device(hass, call.data["device_id"])
if c is None or c._client is None or not c._client.connected:
raise HomeAssistantError(
f"GivEnergy inverter for device {call.data['device_id']!r} "
"is not currently connected"
)
await c._client.one_shot_command(commands.set_calibrate_battery_soc())

hass.services.async_register(
DOMAIN, SERVICE_REBOOT_INVERTER, handle_reboot_inverter, SERVICE_DEVICE_SCHEMA
)
hass.services.async_register(
DOMAIN,
SERVICE_CALIBRATE_BATTERY_SOC,
handle_calibrate_battery_soc,
SERVICE_DEVICE_SCHEMA,
)

return True


Expand All @@ -43,4 +93,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok:
coordinator: GivEnergyUpdateCoordinator = hass.data[DOMAIN].pop(entry.entry_id)
await coordinator.async_close()

if not hass.data.get(DOMAIN):
hass.services.async_remove(DOMAIN, SERVICE_REBOOT_INVERTER)
hass.services.async_remove(DOMAIN, SERVICE_CALIBRATE_BATTERY_SOC)

return unload_ok
9 changes: 5 additions & 4 deletions custom_components/givenergy_local/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@
from homeassistant.const import CONF_HOST, CONF_PORT

from .const import (
CONF_MAX_BATTERIES,
CONF_PASSIVE,
CONF_SCAN_INTERVAL,
CONF_TIMEOUT_TOLERANCE,
DEFAULT_MAX_BATTERIES,
DEFAULT_PASSIVE,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
Expand All @@ -28,7 +26,6 @@
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
vol.Required(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int,
vol.Required(CONF_MAX_BATTERIES, default=DEFAULT_MAX_BATTERIES): int,
vol.Required(CONF_PASSIVE, default=DEFAULT_PASSIVE): bool,
vol.Required(CONF_TIMEOUT_TOLERANCE, default=DEFAULT_TIMEOUT_TOLERANCE): int,
}
Expand Down Expand Up @@ -99,7 +96,11 @@ async def _test_connection(self, host: str, port: int) -> tuple[str, str | None]
client = Client(host=host, port=port)
try:
await client.connect()
plant = await client.refresh_plant(full_refresh=False, max_batteries=0)
# detect() resolves the device model and topology before any reads
# so refresh_plant() picks the right register layout (single vs.
# three-phase) from the first request.
await client.detect()
plant = await client.refresh_plant(full_refresh=False)
return plant.inverter_serial_number, None
except Exception:
_LOGGER.exception("Connection test failed for %s:%s", host, port)
Expand Down
5 changes: 3 additions & 2 deletions custom_components/givenergy_local/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

DEFAULT_PORT = 8899
DEFAULT_SCAN_INTERVAL = 30
DEFAULT_MAX_BATTERIES = 1

CONF_MAX_BATTERIES = "max_batteries"
CONF_SCAN_INTERVAL = "scan_interval"
CONF_PASSIVE = "passive"
CONF_TIMEOUT_TOLERANCE = "timeout_tolerance"
Expand All @@ -13,3 +11,6 @@
DEFAULT_TIMEOUT_TOLERANCE = 5

PLATFORMS = ["sensor", "switch", "number", "select", "time"]

SERVICE_REBOOT_INVERTER = "reboot_inverter"
SERVICE_CALIBRATE_BATTERY_SOC = "calibrate_battery_soc"
22 changes: 11 additions & 11 deletions custom_components/givenergy_local/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
from datetime import datetime, timedelta

from givenergy_modbus.client.client import Client
from givenergy_modbus.model.inverter import SinglePhaseInverter
from givenergy_modbus.model.inverter_threephase import ThreePhaseInverter
from givenergy_modbus.model.plant import Plant
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util

from .const import DOMAIN

InverterModel = SinglePhaseInverter | ThreePhaseInverter

_LOGGER = logging.getLogger(__name__)

# Target interval between full holding-register refreshes in active mode.
Expand All @@ -26,7 +30,6 @@ def __init__(
host: str,
port: int,
scan_interval: int,
max_batteries: int,
passive: bool = False,
timeout_tolerance: int = 5,
) -> None:
Expand All @@ -38,7 +41,6 @@ def __init__(
)
self.host = host
self.port = port
self.max_batteries = max_batteries
self.passive = passive
self.timeout_tolerance = timeout_tolerance
self._client: Client | None = None
Comment thread
dewet22 marked this conversation as resolved.
Expand Down Expand Up @@ -113,10 +115,7 @@ async def _active_update(self) -> Plant:
assert self._client is not None # _async_update_data ensures this
full_refresh = self._active_tick % self._full_refresh_every == 0
self._active_tick += 1
await self._client.refresh_plant(
full_refresh=full_refresh,
max_batteries=self.max_batteries,
)
await self._client.refresh_plant(full_refresh=full_refresh)
return self._client.plant

async def _passive_update(self, reconnecting: bool) -> Plant:
Expand All @@ -127,10 +126,7 @@ async def _passive_update(self, reconnecting: bool) -> Plant:
"""
assert self._client is not None # _async_update_data ensures this
if reconnecting:
await self._client.refresh_plant(
full_refresh=True,
max_batteries=self.max_batteries,
)
await self._client.refresh_plant(full_refresh=True)
return self._client.plant

plant = self._client.plant
Expand Down Expand Up @@ -163,9 +159,13 @@ def _check_cache_freshness(self, plant: Plant) -> None:
# ------------------------------------------------------------------

async def _connect(self) -> None:
"""Open a fresh TCP connection and reset all staleness tracking."""
"""Open a fresh TCP connection, discover topology, and reset staleness tracking."""
self._client = Client(host=self.host, port=self.port)
await self._client.connect()
# detect() populates plant.capabilities, which makes subsequent
# refresh_plant() calls dispatch via model-aware load_config()/refresh()
# — required for three-phase, AIO-HV, EMS and other non-default topologies.
await self._client.detect()
self._last_inverter_time = None
self._unchanged_ticks = 0
self._active_tick = 0
Expand Down
2 changes: 1 addition & 1 deletion custom_components/givenergy_local/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/dewet22/givenergy-hass/issues",
"requirements": [
"givenergy-modbus>=1.3.0,<2.0.0a1"
"givenergy-modbus>=2.0.0,<3.0.0"
],
"version": "0.0.0"
}
5 changes: 2 additions & 3 deletions custom_components/givenergy_local/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from dataclasses import dataclass, field

from givenergy_modbus.client import commands
from givenergy_modbus.model.inverter import Inverter
from homeassistant.components.number import NumberEntity, NumberEntityDescription, NumberMode
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE
Expand All @@ -14,12 +13,12 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import GivEnergyUpdateCoordinator
from .coordinator import GivEnergyUpdateCoordinator, InverterModel


@dataclass(frozen=True, kw_only=True)
class GivEnergyNumberEntityDescription(NumberEntityDescription):
value_fn: Callable[[Inverter], float | None] = field(default=lambda _: None)
value_fn: Callable[[InverterModel], float | None] = field(default=lambda _: None)
set_value_cmd: Callable[[float], list] = field(default=lambda _: [])


Expand Down
6 changes: 3 additions & 3 deletions custom_components/givenergy_local/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass, field

from givenergy_modbus.client import commands
from givenergy_modbus.model.inverter import BatteryPowerMode, Inverter
from givenergy_modbus.model.inverter import BatteryPowerMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand All @@ -13,12 +13,12 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import GivEnergyUpdateCoordinator
from .coordinator import GivEnergyUpdateCoordinator, InverterModel


@dataclass(frozen=True, kw_only=True)
class GivEnergySelectEntityDescription(SelectEntityDescription):
current_option_fn: Callable[[Inverter], str | None] = field(default=lambda _: None)
current_option_fn: Callable[[InverterModel], str | None] = field(default=lambda _: None)
select_option_cmd: Callable[[str], list] = field(default=lambda _: [])


Expand Down
6 changes: 3 additions & 3 deletions custom_components/givenergy_local/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any

from givenergy_modbus.model.battery import Battery
from givenergy_modbus.model.inverter import BatteryType, Inverter, MeterType, Model, Status
from givenergy_modbus.model.inverter import BatteryType, MeterType, Model, Status
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
Expand All @@ -29,12 +29,12 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import GivEnergyUpdateCoordinator
from .coordinator import GivEnergyUpdateCoordinator, InverterModel


@dataclass(frozen=True, kw_only=True)
class GivEnergyInverterSensorDescription(SensorEntityDescription):
value_fn: Callable[[Inverter], Any] = field(default=lambda _: None)
value_fn: Callable[[InverterModel], Any] = field(default=lambda _: None)


@dataclass(frozen=True, kw_only=True)
Expand Down
28 changes: 28 additions & 0 deletions custom_components/givenergy_local/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
reboot_inverter:
name: Reboot inverter
description: >
Restarts the GivEnergy inverter. The inverter will be offline for approximately
30 seconds. Only use this if the inverter is in an unresponsive state.
fields:
device_id:
name: Device
description: The GivEnergy inverter to reboot.
required: true
selector:
device:
integration: givenergy_local

calibrate_battery_soc:
name: Calibrate battery state of charge
description: >
Instructs the inverter to recalibrate its battery state-of-charge estimation.
This triggers a full charge/discharge cycle and should only be run when advised
by GivEnergy support or as part of routine battery maintenance.
fields:
device_id:
name: Device
description: The GivEnergy inverter to calibrate.
required: true
selector:
device:
integration: givenergy_local
2 changes: 0 additions & 2 deletions custom_components/givenergy_local/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"host": "Inverter IP Address",
"port": "Modbus Port",
"scan_interval": "Scan Interval (seconds)",
"max_batteries": "Number of Batteries",
"passive": "Passive mode (listen only — another client drives refreshes)",
"timeout_tolerance": "Timeout Tolerance (consecutive failures before unavailable)"
}
Expand All @@ -20,7 +19,6 @@
"host": "Inverter IP Address",
"port": "Modbus Port",
"scan_interval": "Scan Interval (seconds)",
"max_batteries": "Number of Batteries",
"passive": "Passive mode (listen only — another client drives refreshes)",
"timeout_tolerance": "Timeout Tolerance (consecutive failures before unavailable)"
}
Expand Down
5 changes: 2 additions & 3 deletions custom_components/givenergy_local/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from typing import Any

from givenergy_modbus.client import commands
from givenergy_modbus.model.inverter import Inverter
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand All @@ -14,12 +13,12 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import GivEnergyUpdateCoordinator
from .coordinator import GivEnergyUpdateCoordinator, InverterModel


@dataclass(frozen=True, kw_only=True)
class GivEnergySwitchEntityDescription(SwitchEntityDescription):
is_on_fn: Callable[[Inverter], bool] = field(default=lambda _: False)
is_on_fn: Callable[[InverterModel], bool] = field(default=lambda _: False)
turn_on_cmd: Callable[[], list] = field(default=list)
turn_off_cmd: Callable[[], list] = field(default=list)

Expand Down
Loading
Loading