Skip to content

Commit

Permalink
Add number entities for battery charge/discharge power (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdpuk authored Mar 15, 2023
1 parent 205f782 commit 7732797
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 67 deletions.
3 changes: 3 additions & 0 deletions custom_components/givenergy_local/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

MANUFACTURER = "GivEnergy"

# The nominal voltage of all LiFePO4 packs
BATTERY_NOMINAL_VOLTAGE = 51.2


class Icon(str, Enum):
"""Icon styles."""
Expand Down
11 changes: 11 additions & 0 deletions custom_components/givenergy_local/entity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Home Assistant entity descriptions."""
from givenergy_modbus.model.inverter import Model
from givenergy_modbus.model.plant import Battery, Inverter, Plant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity import DeviceInfo
Expand Down Expand Up @@ -54,6 +55,16 @@ def available(self) -> bool:
"""Return True if the inverter is online."""
return self.coordinator.last_update_success # type: ignore[no-any-return]

@property
def inverter_model(self) -> Model:
"""Get the inverter model."""
return self.data.inverter_model

@property
def inverter_max_battery_power(self) -> Model:
"""Get the maximum battery charge/discharge power for this model."""
return 3600 if self.inverter_model == Model.Gen2 else 2600


class BatteryEntity(CoordinatorEntity[Plant]):
"""An entity associated with a battery device connected to the inverter."""
Expand Down
140 changes: 137 additions & 3 deletions custom_components/givenergy_local/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
from __future__ import annotations

from givenergy_modbus.client import GivEnergyClient
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from givenergy_modbus.model.inverter import Model
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE
from homeassistant.const import PERCENTAGE, POWER_WATT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType

from .const import DOMAIN, Icon
from .const import BATTERY_NOMINAL_VOLTAGE, DOMAIN, Icon
from .coordinator import GivEnergyUpdateCoordinator
from .entity import InverterEntity
from .givenergy_ext import async_reliable_call
Expand All @@ -25,6 +30,8 @@ async def async_setup_entry(
async_add_entities(
[
ACChargeLimitNumber(coordinator, config_entry),
InverterBatteryChargeLimitNumber(coordinator, config_entry),
InverterBatteryDischargeLimitNumber(coordinator, config_entry),
]
)

Expand Down Expand Up @@ -94,3 +101,130 @@ def enable_charge_target(client: GivEnergyClient) -> None:
self.coordinator,
enable_charge_target,
)


class InverterBatteryPowerLimitNumber(InverterBasicNumber):
"""Number to represent a battery charge/discharge rate."""

def __init__(
self,
coordinator: GivEnergyUpdateCoordinator,
config_entry: ConfigEntry,
entity_description: NumberEntityDescription,
) -> None:
"""Initialize the power limit number."""
super().__init__(coordinator, config_entry, entity_description)

# We need to calculate the maximum possible value based on inverter and battery
# capabilities. We know packs are limited to 0.5C charge/discharge, so:
battery_max_power = int(
self.data.battery_nominal_capacity * BATTERY_NOMINAL_VOLTAGE * 0.5
)

# Work out the maximum possible power
self._attr_native_max_value = min(
battery_max_power, self.inverter_max_battery_power
)

# To add confusion to the matter, the raw values used by the API need to be determined
# from the battery capacity
self.battery_power_step = (
self.data.battery_nominal_capacity * BATTERY_NOMINAL_VOLTAGE / 100
)

@property
def native_value(self) -> StateType:
"""Get the current value in Watts."""
raw_value = self.data.dict().get(self.entity_description.key)
power_watts = int(raw_value * self.battery_power_step)
return min(power_watts, self.inverter_max_battery_power)

def watts_to_api_value(self, watts: int) -> int:
"""
Convert a battery power limit (in Watts) to a value used by the inverter API.
There is added complexity here because the API values depend on the battery &
inverter capabilities.
"""
target_value = int(watts / self.battery_power_step)

if self.inverter_model == Model.Gen2:
# Gen2 inverters: Numbering stops at 36, then jumps to 50 = 3.6kW
if target_value > 36:
target_value = 50
else:
# Everything else: Numbering stops at 30, then jumps to 50 = 2.6kW
if target_value > 30:
target_value = 50

return target_value


class InverterBatteryChargeLimitNumber(InverterBatteryPowerLimitNumber):
"""Number to represent a battery charge power limit in Watts."""

def __init__(
self,
coordinator: GivEnergyUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialise the charge power limit number."""
super().__init__(
coordinator,
config_entry,
NumberEntityDescription(
key="battery_charge_limit",
name="Battery Charge Power Limit",
icon=Icon.BATTERY_PLUS,
device_class=NumberDeviceClass.POWER,
native_unit_of_measurement=POWER_WATT,
),
)

async def async_set_native_value(self, value: float) -> None:
"""Update the current charge power limit."""
raw_value = self.watts_to_api_value(int(value))

def set_rate(client: GivEnergyClient) -> None:
client.set_battery_charge_limit(int(raw_value))

await async_reliable_call(
self.hass,
self.coordinator,
set_rate,
)


class InverterBatteryDischargeLimitNumber(InverterBatteryPowerLimitNumber):
"""Number to represent a battery discharge power limit in Watts."""

def __init__(
self,
coordinator: GivEnergyUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialise the discharge power limit number."""
super().__init__(
coordinator,
config_entry,
NumberEntityDescription(
key="battery_discharge_limit",
name="Battery Discharge Power Limit",
icon=Icon.BATTERY_PLUS,
device_class=NumberDeviceClass.POWER,
native_unit_of_measurement=POWER_WATT,
),
)

async def async_set_native_value(self, value: float) -> None:
"""Update the current discharge power limit."""
raw_value = self.watts_to_api_value(int(value))

def set_rate(client: GivEnergyClient) -> None:
client.set_battery_discharge_limit(int(raw_value))

await async_reliable_call(
self.hass,
self.coordinator,
set_rate,
)
62 changes: 0 additions & 62 deletions custom_components/givenergy_local/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,24 +255,6 @@
icon=Icon.BATTERY,
)

_BATTERY_CHARGE_LIMIT_SENSOR = SensorEntityDescription(
key="battery_charge_limit",
name="Battery Charge Power Limit",
icon=Icon.BATTERY_PLUS,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=POWER_WATT,
)

_BATTERY_DISCHARGE_LIMIT_SENSOR = SensorEntityDescription(
key="battery_discharge_limit",
name="Battery Discharge Power Limit",
icon=Icon.BATTERY_MINUS,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=POWER_WATT,
)

_BASIC_BATTERY_SENSORS = [
SensorEntityDescription(
key="battery_soc",
Expand Down Expand Up @@ -351,16 +333,6 @@ async def async_setup_entry(
BatteryModeSensor(
coordinator, config_entry, entity_description=_BATTERY_MODE_SENSOR
),
BatteryChargeLimitSensor(
coordinator,
config_entry,
entity_description=_BATTERY_CHARGE_LIMIT_SENSOR,
),
BatteryDischargeLimitSensor(
coordinator,
config_entry,
entity_description=_BATTERY_DISCHARGE_LIMIT_SENSOR,
),
]
)

Expand Down Expand Up @@ -508,40 +480,6 @@ def native_value(self) -> StateType:
return "Unknown"


class BatteryPowerLimitSensor(InverterBasicSensor):
"""Battery power limit sensor for charging or discharging."""

def convert_raw_power_to_watts(self, raw_value: int) -> int:
"""Convert an inverter register value to a power value in Watts."""
# Here be dragons.
# Interprets the raw value in the same way as the GivEnergy web portal.
# Step values are 81W up to 30*81=2430W.
# It's possible to write raw values of 31, 32 and 50 using the portal and/or
# app, but all of these are rendered as 2600W when read back in the portal.
# At time of writing, the official app uses a slightly different step value.
# Tested on a Gen 1 inverter with +/- 2.6kW max battery power; different logic
# almost certainly required on other units.
return 2600 if raw_value > 30 else raw_value * 81


class BatteryChargeLimitSensor(BatteryPowerLimitSensor):
"""Battery charge limit sensor."""

@property
def native_value(self) -> StateType:
"""Map the low-level value to power in Watts."""
return self.convert_raw_power_to_watts(self.data.battery_charge_limit)


class BatteryDischargeLimitSensor(BatteryPowerLimitSensor):
"""Battery discharge limit sensor."""

@property
def native_value(self) -> StateType:
"""Map the low-level value to power in Watts."""
return self.convert_raw_power_to_watts(self.data.battery_discharge_limit)


class BatteryBasicSensor(BatteryEntity, SensorEntity):
"""
A battery sensor that derives its value from the register values fetched from the inverter.
Expand Down
10 changes: 8 additions & 2 deletions custom_components/givenergy_local/services.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
---
set_charge_limit:
name: Set charge power limit
description: Set the maximum battery charging rate
description: >
Set the maximum battery charging rate. DEPRECATED: Use the
number.battery_charge_power entity instead. This only works for Gen1
inverters and will be removed in a future version.
fields:
device_id:
name: Inverter
Expand All @@ -24,7 +27,10 @@ set_charge_limit:
unit_of_measurement: W
set_discharge_limit:
name: Set discharge power limit
description: Set the maximum battery discharging rate
description: >
Set the maximum battery discharge rate. DEPRECATED: Use the
number.battery_discharge_power entity instead. This only works for Gen1
inverters and will be removed in a future version.
fields:
device_id:
name: Inverter
Expand Down

0 comments on commit 7732797

Please sign in to comment.