Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Power calculation controls for Meter devices #59

Merged
merged 1 commit into from
Oct 18, 2024
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
43 changes: 43 additions & 0 deletions custom_components/garo_wallbox/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.storage import Store

from .garo import ApiClient, GaroConfig, GaroStatus, GaroCharger, GaroMeter, GaroSchema
from .garo.const import CableLockMode, PRODUCT_MAP, GaroProductInfo
from . import const

_LOGGER = logging.getLogger(__name__)

METER_CALCULATE_POWER = 'meter_calculate_power'
METER_VOLTAGE = 'meter_voltage'

class GaroDeviceCoordinator(DataUpdateCoordinator[int]):
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api_client: ApiClient, config: GaroConfig) -> None:
super().__init__(
Expand All @@ -32,6 +36,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api_client: ApiClien
self._slaves = self._config.slaves
self._schema: list[GaroSchema] = []


self._update_id = 0

@property
Expand Down Expand Up @@ -76,6 +81,8 @@ def device_info(self)->DeviceInfo:
hw_version=f"{self._config.firmware_version}.{self._config.firmware_revision}"
)



def get_charger_device_info(self, charger: GaroCharger)->DeviceInfo:
product = self.get_product_info(charger)
return DeviceInfo(
Expand Down Expand Up @@ -163,6 +170,8 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api_client: ApiClien
self._external_meter: GaroMeter | None = None
self._central100_meter: GaroMeter | None = None
self._central101_meter: GaroMeter | None = None
self._store = Store(hass, version=1, key="garo_meter")
self._stored_data: dict|None = None

self._update_id = 0

Expand Down Expand Up @@ -193,6 +202,18 @@ def central101_meter(self)->GaroMeter:
raise ValueError("Central 101 meter not initialized")
return self._central101_meter

@property
def calculate_power(self)->bool:
if self._stored_data is None:
raise ValueError("Stored data is not initialized")
return self._stored_data[METER_CALCULATE_POWER] if METER_CALCULATE_POWER in self._stored_data else True

@property
def voltage(self)->int:
if self._stored_data is None:
raise ValueError("Stored data is not initialized")
return int(self._stored_data[METER_VOLTAGE]) if METER_VOLTAGE in self._stored_data else 230

def get_device_info(self, meter: GaroMeter)->DeviceInfo:

return DeviceInfo(
Expand All @@ -202,10 +223,32 @@ def get_device_info(self, meter: GaroMeter)->DeviceInfo:
model=f"Type {meter.type}",
serial_number=str(meter.serial_number),
)

async def async_set_calculate_power(self, calculate_power:bool):
"""Set calculate power."""
if self._stored_data is None:
raise ValueError("Stored data is not initialized")
_LOGGER.debug(f"Setting calculate power to {calculate_power}")
self._stored_data[METER_CALCULATE_POWER] = calculate_power
await self._store.async_save(self._stored_data)
self.async_update_listeners()

async def async_set_voltage(self, voltage:int):
"""Set voltage."""
if self._stored_data is None:
raise ValueError("Stored data is not initialized")
_LOGGER.debug(f"Setting voltage to {voltage}")
self._stored_data[METER_VOLTAGE] = voltage
await self._store.async_save(self._stored_data)
self.async_update_listeners()


async def _fetch_device_data(self)->int:
try:
has_changed = False
if not self._stored_data:
self._stored_data = await self._store.async_load() or {}
_LOGGER.debug("Loaded stored data: %s", self._stored_data)
if self._config.local_load_balanced:
self._external_meter = await self._api_client.async_get_external_meter(self._external_meter)
if self._external_meter.has_changed:
Expand Down
67 changes: 62 additions & 5 deletions custom_components/garo_wallbox/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


from homeassistant.core import HomeAssistant
from homeassistant.const import EntityCategory
from homeassistant.config_entries import ConfigEntry
from homeassistant.components.number import (
NumberDeviceClass,
Expand All @@ -12,8 +13,8 @@
)

from .garo import GaroStatus, const
from .coordinator import GaroDeviceCoordinator
from .base import GaroEntity
from .coordinator import GaroDeviceCoordinator, GaroMeterCoordinator
from .base import GaroEntity, GaroMeterEntity, GaroMeter
from .const import DOMAIN,COORDINATOR
from . import GaroConfigEntry

Expand All @@ -24,12 +25,19 @@ class GaroNumberEntityDescription(NumberEntityDescription):
set_value: Callable[[int], Awaitable]
is_available: Callable[[], bool] | None = None

@dataclass(frozen=True, kw_only=True)
class GaroMeterNumberEntityDescription(NumberEntityDescription):
"""Describes Garo Number entity."""
get_value: Callable[[GaroMeter], int]
set_value: Callable[[int], Awaitable]
is_available: Callable[[], bool] | None = None

async def async_setup_entry(hass: HomeAssistant, entry: GaroConfigEntry, async_add_entities):
"""Set up using config_entry."""
coordinator = entry.runtime_data.coordinator
api_client = coordinator.api_client
configuration = coordinator.config
async_add_entities([
entities:list[NumberEntity] =[
GaroNumberEntity(coordinator, entry, description) for description in [
GaroNumberEntityDescription(
key="current_limit",
Expand All @@ -44,7 +52,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: GaroConfigEntry, async_a
set_value=lambda value: api_client.async_set_current_limit(value),
is_available=lambda: coordinator.config.charge_limit_enabled,
),
]])
]]
if entry.runtime_data.meter_coordinator:
meter_coordinator = entry.runtime_data.meter_coordinator
def add_meter_entities(meter: GaroMeter):
entities.extend(GaroMeterNumberEntity(meter_coordinator, entry, description, meter) for description in [
GaroMeterNumberEntityDescription(
key="meter_mains_voltage",
translation_key="meter_mains_voltage",
name="Mains voltage",
icon="mdi:sine-wave",
native_max_value=280,
native_min_value=100,
native_step=1,
native_unit_of_measurement="V",
mode=NumberMode.BOX,
entity_category=EntityCategory.DIAGNOSTIC,
get_value=lambda status: meter_coordinator.voltage,
set_value=lambda value: meter_coordinator.async_set_voltage(value),
is_available=lambda: True,
)])
if meter_coordinator.has_external_meter:
add_meter_entities(meter_coordinator.external_meter)
if meter_coordinator.has_central100_meter:
add_meter_entities(meter_coordinator.central100_meter)
if meter_coordinator.has_central101_meter:
add_meter_entities(meter_coordinator.central101_meter)
async_add_entities(entities)


class GaroNumberEntity(GaroEntity, NumberEntity):
Expand All @@ -68,4 +102,27 @@ async def async_set_native_value(self, value: float) -> None:
self.async_write_ha_state()

def _async_update_attrs(self) -> None:
self._attr_native_value = self.entity_description.get_value(self.coordinator.status)
self._attr_native_value = self.entity_description.get_value(self.coordinator.status)

class GaroMeterNumberEntity(GaroMeterEntity, NumberEntity):

entity_description: GaroMeterNumberEntityDescription

def __init__(self, coordinator: GaroMeterCoordinator, entry, description: GaroMeterNumberEntityDescription, meter: GaroMeter):
self.entity_description = description
super().__init__(coordinator, entry, description.key, meter)

@property
def available(self) -> bool:
"""Return if entity is available."""
return self.entity_description.is_available() and super().available if self.entity_description.is_available else super().available

async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
value = int(value)
await self.entity_description.set_value(value)
self._attr_native_value = value
self.async_write_ha_state()

def _async_update_attrs(self) -> None:
self._attr_native_value = self.entity_description.get_value(self._meter)
8 changes: 4 additions & 4 deletions custom_components/garo_wallbox/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ def add_meter_sentities(meter: GaroMeter, is_3_phase: bool = True):
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
get_state=lambda meter: meter.l1_power,
get_state=lambda meter: round(meter.l1_current*meter_coordinator.voltage, -1)/1000 if meter_coordinator.calculate_power else meter.l1_power,
),
GaroMeterSensorEntityDescription(
key="meter_l2_power",
Expand All @@ -469,7 +469,7 @@ def add_meter_sentities(meter: GaroMeter, is_3_phase: bool = True):
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
get_state=lambda meter: meter.l2_power,
get_state=lambda meter: round(meter.l2_current*meter_coordinator.voltage, -1)/1000 if meter_coordinator.calculate_power else meter.l2_power,
entity_registry_enabled_default=is_3_phase,
),
GaroMeterSensorEntityDescription(
Expand All @@ -479,7 +479,7 @@ def add_meter_sentities(meter: GaroMeter, is_3_phase: bool = True):
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
get_state=lambda meter: meter.l3_power,
get_state=lambda meter: round(meter.l3_current*meter_coordinator.voltage, -1)/1000 if meter_coordinator.calculate_power else meter.l3_power,
entity_registry_enabled_default=is_3_phase,
),
GaroMeterSensorEntityDescription(
Expand All @@ -489,7 +489,7 @@ def add_meter_sentities(meter: GaroMeter, is_3_phase: bool = True):
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
get_state=lambda meter: meter.apparent_power,
get_state=lambda meter: round((meter.l1_current+meter.l2_current+meter.l3_current)*meter_coordinator.voltage, -1)/1000 if meter_coordinator.calculate_power else meter.apparent_power,
),
GaroMeterSensorEntityDescription(
key="meter_accumulated_energy",
Expand Down
59 changes: 55 additions & 4 deletions custom_components/garo_wallbox/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from dataclasses import dataclass

from homeassistant.core import HomeAssistant
from homeassistant.const import EntityCategory
from homeassistant.config_entries import ConfigEntry
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription


from .coordinator import GaroDeviceCoordinator
from .base import GaroEntity
from .coordinator import GaroDeviceCoordinator, GaroMeterCoordinator
from .base import GaroEntity, GaroMeter, GaroMeterEntity
from .const import DOMAIN,COORDINATOR
from . import GaroConfigEntry

Expand All @@ -22,7 +23,7 @@ class GaroSwitchEntityDescription(SwitchEntityDescription):
async def async_setup_entry(hass: HomeAssistant, entry: GaroConfigEntry, async_add_entities):
"""Set up using config_entry."""
coordinator = entry.runtime_data.coordinator
async_add_entities([
entities:list[SwitchEntity] = [
PanasonicSwitchEntity(coordinator, entry, description) for description in [
GaroSwitchEntityDescription(
key="charge_limit",
Expand All @@ -33,7 +34,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: GaroConfigEntry, async_a
off_func=lambda: coordinator.async_enable_charge_limit(False),
get_state=lambda: coordinator.config.charge_limit_enabled,
),
]])
]]
if entry.runtime_data.meter_coordinator:
meter_coordinator = entry.runtime_data.meter_coordinator
def add_meter_entities(meter: GaroMeter):
entities.extend(GaroMeterSwitchEntity(meter_coordinator, entry, description, meter) for description in [
GaroSwitchEntityDescription(
key="meter_calculate_power",
translation_key="meter_calculate_power",
name="Use calulated power values",
icon="mdi:calculator-variant",
entity_category=EntityCategory.DIAGNOSTIC,
on_func=lambda: meter_coordinator.async_set_calculate_power(True),
off_func=lambda: meter_coordinator.async_set_calculate_power(False),
get_state=lambda: meter_coordinator.calculate_power,
)])
if meter_coordinator.has_external_meter:
add_meter_entities(meter_coordinator.external_meter)
if meter_coordinator.has_central100_meter:
add_meter_entities(meter_coordinator.central100_meter)
if meter_coordinator.has_central101_meter:
add_meter_entities(meter_coordinator.central101_meter)

async_add_entities(entities)


class PanasonicSwitchEntity(GaroEntity, SwitchEntity):
"""Representation of a Garo switch."""
Expand All @@ -51,6 +75,33 @@ def _async_update_attrs(self) -> None:
self._attr_is_on = self.entity_description.get_state()


async def async_turn_on(self, **kwargs):
"""Turn on the Switch."""
await self.entity_description.on_func()
self._attr_is_on = True
self.async_write_ha_state()

async def async_turn_off(self, **kwargs):
"""Turn off the Switch."""
await self.entity_description.off_func()
self._attr_is_on = False
self.async_write_ha_state()

class GaroMeterSwitchEntity(GaroMeterEntity, SwitchEntity):
"""Representation of a Garo switch."""
entity_description: GaroSwitchEntityDescription

def __init__(self, coordinator: GaroMeterCoordinator, entry, description: GaroSwitchEntityDescription, meter: GaroMeter):
"""Initialize the Switch."""
self.entity_description = description
super().__init__(coordinator, entry, description.key, meter)


def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_is_on = self.entity_description.get_state()


async def async_turn_on(self, **kwargs):
"""Turn on the Switch."""
await self.entity_description.on_func()
Expand Down