Skip to content

Commit

Permalink
Givenergy stable release
Browse files Browse the repository at this point in the history
  • Loading branch information
kylegordon committed Sep 22, 2024
1 parent 33ef613 commit fdd76c7
Show file tree
Hide file tree
Showing 11 changed files with 33 additions and 313 deletions.
2 changes: 0 additions & 2 deletions custom_components/givenergy_local/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SENSOR,
Platform.SELECT,
Platform.SWITCH,
Platform.TIME,
]


Expand Down
21 changes: 13 additions & 8 deletions custom_components/givenergy_local/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,15 @@ def _schedule_next_update(self) -> None:
Work out when we next need to update the state due to the current time
passing over the start of end time of the slot.
"""
if (slot := self.slot) is None:
return

now = dt.now()

# Get slot details
current_time = now.time()
start = self.slot.start
end = self.slot.end
start = slot.start
end = slot.end

# We don't need to be notified about entering/leaving an undefined slot
if start == end:
Expand Down Expand Up @@ -146,22 +149,24 @@ def _handle_coordinator_update(self) -> None:
self.async_write_ha_state()

@property
def slot(self) -> TimeSlot:
def slot(self) -> TimeSlot | None:
"""Get the slot definition."""
slot: TimeSlot = self.data.dict().get(self.entity_description.key)
return slot

@property
def is_on(self) -> bool | None:
"""Determine whether we're currently within the slot."""
now: time = dt.now().time()
is_on: bool = self.slot.start <= now < self.slot.end
return is_on
if slot := self.slot:
now: time = dt.now().time()
is_on: bool = slot.start <= now < self.slot.end
return is_on
return False

@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Attach charge slot configuration."""
return {
"start": self.slot.start.strftime("%H:%M"),
"end": self.slot.end.strftime("%H:%M"),
"start": self.slot.start.strftime("%H:%M") if self.slot else None,
"end": self.slot.end.strftime("%H:%M") if self.slot else None,
}
5 changes: 2 additions & 3 deletions custom_components/givenergy_local/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Constants for the GivEnergy integration."""

from enum import StrEnum
from enum import Enum
from logging import Logger, getLogger

DOMAIN = "givenergy_local"
Expand All @@ -14,7 +14,7 @@
BATTERY_NOMINAL_VOLTAGE = 51.2


class Icon(StrEnum):
class Icon(str, Enum):
"""Icon styles."""

PV = "mdi:solar-power"
Expand All @@ -24,7 +24,6 @@ class Icon(StrEnum):
BATTERY_TEMPERATURE = "mdi:thermometer"
BATTERY_MINUS = "mdi:battery-minus"
BATTERY_PLUS = "mdi:battery-plus"
BATTERY_PAUSE = "mdi:battery-clock"
INVERTER = "mdi:flash"
GRID_IMPORT = "mdi:transmission-tower-export"
GRID_EXPORT = "mdi:transmission-tower-import"
Expand Down
5 changes: 1 addition & 4 deletions custom_components/givenergy_local/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,7 @@ async def _async_update_data(self) -> Plant:
"""Fetch data from the inverter."""
if not self.client.connected:
await self.client.connect()
await self.client.detect_plant()
self.require_full_refresh = False
self.last_full_refresh = datetime.utcnow()
return self.client.plant
self.require_full_refresh = True

if self.last_full_refresh < (datetime.utcnow() - _FULL_REFRESH_INTERVAL):
self.require_full_refresh = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,35 +75,6 @@ async def connect(self) -> None:
self.connected = True
_logger.info("Connection established to %s:%d", self.host, self.port)

async def detect_plant(self, timeout: int = 1, retries: int = 3) -> None:
"""Detect inverter capabilities that influence how subsequent requests are made."""
_logger.info("Detectig plant")

# Refresh the core set of registers that work across all inverters
await self.refresh_plant(True, timeout=timeout, retries=retries)

# Use that to detect the number of batteries
self.plant.detect_batteries()
_logger.info("Batteries detected: %d", self.plant.number_batteries)

# Some devices support additional registers
# When unsupported, devices appear to simple ignore requests
possible_additional_holding_registers = [300]
for hr in possible_additional_holding_registers:
try:
reqs = commands.refresh_additional_holding_registers(hr)
await self.execute(reqs, timeout=timeout, retries=retries)
_logger.info(
"Detected additional holding register support (base_register=%d)",
hr,
)
self.plant.additional_holding_registers.append(hr)
except asyncio.TimeoutError:
_logger.debug(
"Inverter did not respond to holder register query (base_register=%d)",
hr,
)

async def close(self) -> None:
"""Disconnect from the remote host and clean up tasks and queues."""
if not self.connected:
Expand Down Expand Up @@ -153,6 +124,10 @@ async def refresh_plant(
full_refresh, self.plant.number_batteries, max_batteries
)
await self.execute(reqs, timeout=timeout, retries=retries)

if full_refresh:
self.plant.detect_batteries()

return self.plant

async def watch_plant(
Expand All @@ -167,6 +142,7 @@ async def watch_plant(
"""Refresh data about the Plant."""
await self.connect()
await self.refresh_plant(True, max_batteries=max_batteries)
self.plant.detect_batteries()
while True:
if handler:
handler()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
"""High-level methods for interacting with a remote system."""

from datetime import datetime
from time import time
from typing import Optional

from typing_extensions import deprecated # type: ignore[attr-defined]

from custom_components.givenergy_local.givenergy_modbus.model import TimeSlot
from custom_components.givenergy_local.givenergy_modbus.model.inverter import (
BatteryPauseMode,
)
from custom_components.givenergy_local.givenergy_modbus.pdu import (
ReadHoldingRegistersRequest,
ReadInputRegistersRequest,
Expand Down Expand Up @@ -47,30 +43,10 @@ class RegisterMap:
BATTERY_DISCHARGE_MIN_POWER_RESERVE = 114
CHARGE_TARGET_SOC = 116
REBOOT = 163
BATTERY_PAUSE_MODE = 318
BATTERY_PAUSE_SLOT_START = 319
BATTERY_PAUSE_SLOT_END = 320


def refresh_additional_holding_registers(
base_register: int,
) -> list[TransparentRequest]:
"""Requests one specific set of holding registers.
This is intended to be used in cases where registers may or may not be present,
depending on device capabilities."""
return [
ReadHoldingRegistersRequest(
base_register=base_register, register_count=60, slave_address=0x32
)
]


def refresh_plant_data(
complete: bool,
number_batteries: int = 1,
max_batteries: int = 5,
additional_holding_registers: Optional[list[int]] = None,
complete: bool, number_batteries: int = 1, max_batteries: int = 5
) -> list[TransparentRequest]:
"""Refresh plant data."""
requests: list[TransparentRequest] = [
Expand Down Expand Up @@ -102,11 +78,6 @@ def refresh_plant_data(
base_register=120, register_count=60, slave_address=0x32
)
)

if additional_holding_registers:
for hr in additional_holding_registers:
requests.extend(refresh_additional_holding_registers(hr))

number_batteries = max_batteries
for i in range(number_batteries):
requests.append(
Expand Down Expand Up @@ -238,13 +209,6 @@ def set_battery_power_reserve(val: int) -> list[TransparentRequest]:
]


def set_battery_pause_mode(val: BatteryPauseMode) -> list[TransparentRequest]:
"""Set the battery pause mode."""
if not 0 <= val <= 3:
raise ValueError(f"Battery pause mode ({val}) must be in [0-3]")
return [WriteHoldingRegisterRequest(RegisterMap.BATTERY_PAUSE_MODE, val)]


def _set_charge_slot(
discharge: bool, idx: int, slot: Optional[TimeSlot]
) -> list[TransparentRequest]:
Expand All @@ -264,32 +228,6 @@ def _set_charge_slot(
]


def set_pause_slot_start(start: Optional[time]) -> list[TransparentRequest]:
if start:
return [
WriteHoldingRegisterRequest(
RegisterMap.BATTERY_PAUSE_SLOT_START, int(start.strftime("%H%M"))
),
]
else:
return [
WriteHoldingRegisterRequest(RegisterMap.BATTERY_PAUSE_SLOT_START, 0),
]


def set_pause_slot_end(end: Optional[time]) -> list[TransparentRequest]:
if end:
return [
WriteHoldingRegisterRequest(
RegisterMap.BATTERY_PAUSE_SLOT_END, int(end.strftime("%H%M"))
),
]
else:
return [
WriteHoldingRegisterRequest(RegisterMap.BATTERY_PAUSE_SLOT_END, 0),
]


def set_charge_slot_1(timeslot: TimeSlot) -> list[TransparentRequest]:
"""Set first charge slot start & end times."""
return _set_charge_slot(False, 1, timeslot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,6 @@ class BatteryType(IntEnum):
LITHIUM = 1


class BatteryPauseMode(IntEnum):
"""Battery pause mode."""

DISABLED = 0
PAUSE_CHARGE = 1
PAUSE_DISCHARGE = 2
PAUSE_BOTH = 3


class PowerFactorFunctionModel(IntEnum):
"""Power Factor function model."""

Expand Down Expand Up @@ -242,11 +233,6 @@ class InverterRegisterGetter(RegisterGetter):
"enable_standard_self_consumption_logic": Def(C.bool, None, HR(199)),
"cmd_bms_flash_update": Def(C.bool, None, HR(200)),
#
# Holding Registers, block 300-359
#
"battery_pause_mode": Def(C.uint16, BatteryPauseMode, HR(318)),
"battery_pause_slot_1": Def(C.timeslot, None, HR(319), HR(320)),
#
# Holding Registers, block 4080-4139
#
"pv_power_setting": Def(C.uint32, None, HR(4107), HR(4108)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class Plant(GivEnergyBaseModel):
"""Representation of a complete GivEnergy plant."""

register_caches: dict[int, RegisterCache] = {}
additional_holding_registers: list[int] = []
inverter_serial_number: str = ""
data_adapter_serial_number: str = ""
number_batteries: int = 0
Expand Down Expand Up @@ -94,6 +93,7 @@ def detect_batteries(self) -> None:
assert Battery.from_orm(self.register_caches[i + 0x32]).is_valid()
except (KeyError, AssertionError):
break
_logger.debug("Updating connected battery count to %d", i)
self.number_batteries = i

@property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,17 @@ def uint32(high_val: int, low_val: int) -> int:
return (high_val << 16) + low_val

@staticmethod
def timeslot(start_time: int, end_time: int) -> TimeSlot:
def timeslot(start_time: int, end_time: int) -> Optional[TimeSlot]:
"""Interpret register as a time slot."""
if start_time == 60 or end_time == 60:
# Probably due to the inverter holding an invalid value in a timeslot.
# Real life example register values include:
# - [0, 60]
# - [60, 2359]
# Clearly '60' is the problem value in such cases. This seems to correspond to the
# GivEnergy portal displaying '--:--', which we can only assume means 'undefined'.
return None

if start_time is not None and end_time is not None:
return TimeSlot.from_repr(start_time, end_time)

Expand Down
78 changes: 0 additions & 78 deletions custom_components/givenergy_local/select.py

This file was deleted.

Loading

0 comments on commit fdd76c7

Please sign in to comment.