Skip to content

Commit

Permalink
* New luxtronik key management
Browse files Browse the repository at this point in the history
  • Loading branch information
BenPru committed Nov 7, 2023
1 parent 4cea603 commit 8ea8257
Show file tree
Hide file tree
Showing 20 changed files with 1,430 additions and 1,171 deletions.
107 changes: 54 additions & 53 deletions custom_components/luxtronik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
PLATFORMS,
SERVICE_WRITE,
SERVICE_WRITE_SCHEMA,
SensorKey as SK,
Parameter_SensorKey as LP,
Calculation_SensorKey as LC,
)
from .coordinator import LuxtronikCoordinator

Expand Down Expand Up @@ -127,13 +128,13 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
prefix = None
ent_reg = None

def _up(ident: str, new_id: SK, platform: P = P.SENSOR) -> None:
def _up(ident: str, new_id: LC | LP, platform: P = P.SENSOR) -> None:
nonlocal prefix, ent_reg
if prefix is None or ent_reg is None:
prefix = config_entry.data[CONF_HA_SENSOR_PREFIX]
ent_reg = async_get(hass)
entity_id = f"{platform}.{prefix}_{ident}"
new_ident = f"{platform}.{prefix}_{new_id}"
new_ident = f"{platform}.{prefix}_{new_id.name.lower()}"
try:
ent_reg.async_update_entity(
entity_id, new_entity_id=new_ident, new_unique_id=new_ident
Expand All @@ -160,119 +161,119 @@ def _up(ident: str, new_id: SK, platform: P = P.SENSOR) -> None:
hass.config_entries.async_update_entry(config_entry, data=new_data)

if config_entry.version == 5:
_up("heat_amount_domestic_water", SK.DHW_HEAT_AMOUNT)
_up("domestic_water_energy_input", SK.DHW_ENERGY_INPUT)
_up("domestic_water_temperature", SK.DHW_TEMPERATURE)
_up("operation_hours_domestic_water", SK.DHW_OPERATION_HOURS)
_up("domestic_water_target_temperature", SK.DHW_TARGET_TEMPERATURE, P.NUMBER)
_up("domestic_water_hysteresis", SK.DHW_HYSTERESIS, P.NUMBER)
_up("heat_amount_domestic_water", LC.DHW_HEAT_AMOUNT)
_up("domestic_water_energy_input", LP.DHW_ENERGY_INPUT)
_up("domestic_water_temperature", LC.DHW_TEMPERATURE)
_up("operation_hours_domestic_water", LC.DHW_OPERATION_HOURS)
_up("domestic_water_target_temperature", LP.DHW_TARGET_TEMPERATURE, P.NUMBER)
_up("domestic_water_hysteresis", LP.DHW_HYSTERESIS, P.NUMBER)
_up(
"domestic_water_thermal_desinfection_target",
SK.DHW_THERMAL_DESINFECTION_TARGET,
LP.DHW_THERMAL_DESINFECTION_TARGET,
P.NUMBER,
)
_up(
"domestic_water_recirculation_pump",
SK.DHW_RECIRCULATION_PUMP,
LC.DHW_RECIRCULATION_PUMP,
P.BINARY_SENSOR,
)
_up(
"domestic_water_circulation_pump",
SK.DHW_CIRCULATION_PUMP,
LC.DHW_CIRCULATION_PUMP,
P.BINARY_SENSOR,
)
_up("domestic_water_charging_pump", SK.DHW_CHARGING_PUMP, P.BINARY_SENSOR)
_up("domestic_water_charging_pump", LP.DHW_CHARGING_PUMP, P.BINARY_SENSOR)

# [sensor]
_up("pump_frequency", SK.PUMP_FREQUENCY, P.SENSOR)
_up("room_thermostat_temperature", SK.ROOM_THERMOSTAT_TEMPERATURE, P.SENSOR)
_up("pump_frequency", LC.PUMP_FREQUENCY, P.SENSOR)
_up("room_thermostat_temperature", LC.ROOM_THERMOSTAT_TEMPERATURE, P.SENSOR)
_up(
"room_thermostat_temperature_target",
SK.ROOM_THERMOSTAT_TEMPERATURE_TARGET,
LC.ROOM_THERMOSTAT_TEMPERATURE_TARGET,
P.SENSOR,
)

# [binary sensor]
_up("evu_unlocked", SK.EVU_UNLOCKED, P.BINARY_SENSOR)
_up("compressor", SK.COMPRESSOR, P.BINARY_SENSOR)
_up("pump_flow", SK.PUMP_FLOW, P.BINARY_SENSOR)
_up("compressor_heater", SK.COMPRESSOR_HEATER, P.BINARY_SENSOR)
_up("defrost_valve", SK.DEFROST_VALVE, P.BINARY_SENSOR)
_up("additional_heat_generator", SK.ADDITIONAL_HEAT_GENERATOR, P.BINARY_SENSOR)
_up("disturbance_output", SK.DISTURBANCE_OUTPUT, P.BINARY_SENSOR)
_up("circulation_pump_heating", SK.CIRCULATION_PUMP_HEATING, P.BINARY_SENSOR)
_up("evu_unlocked", LC.EVU_UNLOCKED, P.BINARY_SENSOR)
_up("compressor", LC.COMPRESSOR, P.BINARY_SENSOR)
_up("pump_flow", LC.PUMP_FLOW, P.BINARY_SENSOR)
_up("compressor_heater", LC.COMPRESSOR_HEATER, P.BINARY_SENSOR)
_up("defrost_valve", LC.DEFROST_VALVE, P.BINARY_SENSOR)
_up("additional_heat_generator", LC.ADDITIONAL_HEAT_GENERATOR, P.BINARY_SENSOR)
_up("disturbance_output", LC.DISTURBANCE_OUTPUT, P.BINARY_SENSOR)
_up("circulation_pump_heating", LC.CIRCULATION_PUMP_HEATING, P.BINARY_SENSOR)
_up(
"additional_circulation_pump",
SK.ADDITIONAL_CIRCULATION_PUMP,
LC.ADDITIONAL_CIRCULATION_PUMP,
P.BINARY_SENSOR,
)
_up("approval_cooling", SK.APPROVAL_COOLING, P.BINARY_SENSOR)
_up("approval_cooling", LC.APPROVAL_COOLING, P.BINARY_SENSOR)

# [number]
_up("release_second_heat_generator", SK.RELEASE_SECOND_HEAT_GENERATOR, P.NUMBER)
_up("release_second_heat_generator", LP.RELEASE_SECOND_HEAT_GENERATOR, P.NUMBER)
_up(
"release_time_second_heat_generator",
SK.RELEASE_TIME_SECOND_HEAT_GENERATOR,
LP.RELEASE_TIME_SECOND_HEAT_GENERATOR,
P.NUMBER,
)
_up("heating_target_correction", SK.HEATING_TARGET_CORRECTION, P.NUMBER)
_up("pump_optimization_time", SK.PUMP_OPTIMIZATION_TIME, P.NUMBER)
_up("heating_threshold_temperature", SK.HEATING_THRESHOLD_TEMPERATURE, P.NUMBER)
_up("heating_target_correction", LP.HEATING_TARGET_CORRECTION, P.NUMBER)
_up("pump_optimization_time", LP.PUMP_OPTIMIZATION_TIME, P.NUMBER)
_up("heating_threshold_temperature", LP.HEATING_THRESHOLD_TEMPERATURE, P.NUMBER)
_up(
"heating_min_flow_out_temperature",
SK.HEATING_MIN_FLOW_OUT_TEMPERATURE,
LP.HEATING_MIN_FLOW_OUT_TEMPERATURE,
P.NUMBER,
)
_up(
"heating_circuit_curve1_temperature",
SK.HEATING_CIRCUIT_CURVE1_TEMPERATURE,
LP.HEATING_CIRCUIT_CURVE1_TEMPERATURE,
P.NUMBER,
)
_up(
"heating_circuit_curve2_temperature",
SK.HEATING_CIRCUIT_CURVE2_TEMPERATURE,
LP.HEATING_CIRCUIT_CURVE2_TEMPERATURE,
P.NUMBER,
)
_up(
"heating_circuit_curve_night_temperature",
SK.HEATING_CIRCUIT_CURVE_NIGHT_TEMPERATURE,
LP.HEATING_CIRCUIT_CURVE_NIGHT_TEMPERATURE,
P.NUMBER,
)
_up(
"heating_night_lowering_to_temperature",
SK.HEATING_NIGHT_LOWERING_TO_TEMPERATURE,
LP.HEATING_NIGHT_LOWERING_TO_TEMPERATURE,
P.NUMBER,
)
_up("heating_hysteresis", SK.HEATING_HYSTERESIS, P.NUMBER)
_up("heating_hysteresis", LP.HEATING_HYSTERESIS, P.NUMBER)
_up(
"heating_max_flow_out_increase_temperature",
SK.HEATING_MAX_FLOW_OUT_INCREASE_TEMPERATURE,
LP.HEATING_MAX_FLOW_OUT_INCREASE_TEMPERATURE,
P.NUMBER,
)
_up(
"heating_maximum_circulation_pump_speed",
SK.HEATING_MAXIMUM_CIRCULATION_PUMP_SPEED,
LP.HEATING_MAXIMUM_CIRCULATION_PUMP_SPEED,
P.NUMBER,
)
_up(
"heating_room_temperature_impact_factor",
SK.HEATING_ROOM_TEMPERATURE_IMPACT_FACTOR,
LP.HEATING_ROOM_TEMPERATURE_IMPACT_FACTOR,
P.NUMBER,
)

# [switch]
_up("remote_maintenance", SK.REMOTE_MAINTENANCE, P.SWITCH)
_up("efficiency_pump", SK.EFFICIENCY_PUMP, P.SWITCH)
_up("pump_heat_control", SK.PUMP_HEAT_CONTROL, P.SWITCH)
_up("heating", SK.HEATING, P.SWITCH)
_up("pump_optimization", SK.PUMP_OPTIMIZATION, P.SWITCH)
_up("heating_threshold", SK.HEATING_THRESHOLD, P.SWITCH)
_up("domestic_water", SK.DOMESTIC_WATER, P.SWITCH)
_up("cooling", SK.COOLING, P.SWITCH)
_up("remote_maintenance", LP.REMOTE_MAINTENANCE, P.SWITCH)
_up("efficiency_pump", LP.EFFICIENCY_PUMP, P.SWITCH)
_up("pump_heat_control", LP.PUMP_HEAT_CONTROL, P.SWITCH)
_up("heating", LP.HEATING, P.SWITCH)
_up("pump_optimization", LP.PUMP_OPTIMIZATION, P.SWITCH)
_up("heating_threshold", LP.HEATING_THRESHOLD, P.SWITCH)
_up("domestic_water", LP.DOMESTIC_WATER, P.SWITCH)
_up("cooling", LP.COOLING, P.SWITCH)

# [climate]
_up("heating", SK.HEATING, P.CLIMATE)
_up("cooling", SK.COOLING, P.CLIMATE)
_up("heating", LP.HEATING, P.CLIMATE)
_up("cooling", LP.COOLING, P.CLIMATE)

new_data = {**config_entry.data}
config_entry.version = 6
Expand All @@ -282,10 +283,10 @@ def _up(ident: str, new_id: SK, platform: P = P.SENSOR) -> None:

if config_entry.version == 6:
_up(
"cooling_threshold_temperature", SK.COOLING_OUTDOOR_TEMP_THRESHOLD, P.NUMBER
"cooling_threshold_temperature", LP.COOLING_OUTDOOR_TEMP_THRESHOLD, P.NUMBER
)
_up("cooling_start_delay_hours", SK.COOLING_START_DELAY_HOURS, P.NUMBER)
_up("cooling_stop_delay_hours", SK.COOLING_STOP_DELAY_HOURS, P.NUMBER)
_up("cooling_start_delay_hours", LP.COOLING_START_DELAY_HOURS, P.NUMBER)
_up("cooling_stop_delay_hours", LP.COOLING_STOP_DELAY_HOURS, P.NUMBER)

new_data = {**config_entry.data}
config_entry.version = 7
Expand Down
66 changes: 52 additions & 14 deletions custom_components/luxtronik/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from typing import Any

from homeassistant.backports.enum import StrEnum
from homeassistant.components.sensor import RestoreSensor
from homeassistant.components.sensor import RestoreSensor, SensorEntityDescription
from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass
from homeassistant.components.water_heater import STATE_HEAT_PUMP
from homeassistant.const import STATE_OFF, UnitOfTemperature, UnitOfTime
from homeassistant.core import callback
Expand All @@ -18,12 +19,16 @@

from .common import get_sensor_data
from .const import (
UNIT_DEVICE_CLASS_MAP,
DOMAIN,
UNIT_ICON_MAP,
UNIT_STATE_CLASS_MAP,
DeviceKey,
LOGGER,
LuxCalculation as LC,
Calculation_SensorKey as LC,
LuxMode,
LuxOperationMode,
LuxParameter as LP,
Parameter_SensorKey as LP,
SensorAttrFormat,
SensorAttrKey as SA,
)
Expand Down Expand Up @@ -55,8 +60,9 @@ def __init__(
"""Init LuxtronikEntity."""
super().__init__(coordinator=coordinator)
self._device_info_ident = device_info_ident
self.enrich_description(description)
self._attr_extra_state_attributes = {
SA.LUXTRONIK_KEY: f"{description.luxtronik_key.name[1:5]} {description.luxtronik_key.value}"
SA.LUXTRONIK_KEY: f"{str(description.luxtronik_key.__class__)[7:].split('_')[0]} {description.luxtronik_key.value}"
}
for field in description.__dataclass_fields__:
if field.startswith("luxtronik_key_"):
Expand All @@ -70,7 +76,7 @@ def __init__(
else:
self._attr_extra_state_attributes[field] = value
if description.translation_key is None:
description.translation_key = description.key.value
description.translation_key = description.key
if description.entity_registry_enabled_default:
description.entity_registry_enabled_default = coordinator.entity_visible(
description
Expand All @@ -79,14 +85,20 @@ def __init__(
self._attr_device_info = coordinator.get_device(device_info_ident)

translation_key = (
description.key.value
description.key
if description.translation_key_name is None
else description.translation_key_name
)
description.translation_key = translation_key
description.has_entity_name = True
self._attr_state = self._get_value(description.luxtronik_key)

def enrich_description(self, d: LuxtronikEntityDescription) -> None:
d.key = d.luxtronik_key.name.lower()
d.icon = d.icon or UNIT_ICON_MAP.get(d.native_unit_of_measurement)
d.state_class = d.state_class or UNIT_STATE_CLASS_MAP.get(d.native_unit_of_measurement)
d.device_class = d.device_class or UNIT_DEVICE_CLASS_MAP.get(d.native_unit_of_measurement)

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
Expand Down Expand Up @@ -118,6 +130,11 @@ async def async_added_to_hass(self) -> None:
async_dispatcher_connect(
self.hass, data_updated, self._schedule_immediate_update
)
except AttributeError as err:
LOGGER.warning(
"Could not restore latest data (async_added_to_hass)",
exc_info=err,
)
except Exception as err:
LOGGER.error(
"Could not restore latest data (async_added_to_hass)",
Expand All @@ -138,7 +155,10 @@ def _handle_coordinator_update(self) -> None:
# Ensure timezone:
time_zone = dt_util.get_time_zone(self.hass.config.time_zone)
value = value.replace(tzinfo=time_zone)
elif isinstance(descr, SensorEntityDescription):
value = descr.options[value]

last_state = self._attr_state
self._attr_state = value

# Calc icon:
Expand All @@ -164,16 +184,34 @@ def _handle_coordinator_update(self) -> None:
):
self.next_update = utcnow() + descr.update_interval
super()._handle_coordinator_update()
if last_state != self._attr_state:
self.handle_value_change(self._attr_state, last_state)

def handle_value_change(self, value, last_value):
self.fire_event(self.entity_description.event_id_on_change, value, last_value)

def fire_event(self, event: str | None, value, last_value):
if event is None:
return
_event = f"{DOMAIN}_{event}"
data = {
"unique_id": self.unique_id,
"value": value,
"last_value": last_value,
}
self.hass.bus.fire(_event, data)

def _enrich_extra_attributes(self) -> None:
for attr in self.entity_description.extra_attributes:
if attr.format is None and (
attr.luxtronik_key is None or attr.luxtronik_key == LP.UNSET
):
continue
self._attr_extra_state_attributes[attr.key.value] = self.formatted_data(
attr
)
if attr.default_value is not None:
self._attr_extra_state_attributes[attr.key.value] = "_"
else:
self._attr_extra_state_attributes[attr.key.value] = self.formatted_data(
attr
)

@callback
def _schedule_immediate_update(self):
Expand All @@ -199,14 +237,14 @@ def formatted_data(self, attr: LuxtronikEntityAttributeDescription) -> str:
return f"{value/10:.1f} {UnitOfTemperature.CELSIUS}"
if attr.format == SensorAttrFormat.SWITCH_GAP:
flow_out_target = float(
self._get_value(LC.C0012_FLOW_OUT_TEMPERATURE_TARGET)
self._get_value(LC.FLOW_OUT_TEMPERATURE_TARGET)
)
flow_out = float(value)
hyst = float(self._get_value(LP.P0088_HEATING_HYSTERESIS)) * 0.1
hyst = float(self._get_value(LP.HEATING_HYSTERESIS)) * 0.1

if self._get_value(LC.C0080_STATUS) == LuxOperationMode.heating:
if self._get_value(LC.STATUS) == LuxOperationMode.heating:
return f"{flow_out + hyst - flow_out_target:.1f} {UnitOfTemperature.KELVIN}"
if self._get_value(LP.P0003_MODE_HEATING) != LuxMode.off:
if self._get_value(LP.MODE_HEATING) != LuxMode.off:
return f"{flow_out - hyst - flow_out_target:.1f} {UnitOfTemperature.KELVIN}"
return ""

Expand Down
13 changes: 11 additions & 2 deletions custom_components/luxtronik/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def __init__(
self.entity_id = ENTITY_ID_FORMAT.format(f"{prefix}_{description.key}")
self._attr_unique_id = self.entity_id
self._sensor_data = get_sensor_data(
coordinator.data, description.luxtronik_key.value
coordinator.data, description.luxtronik_key
)

hass.bus.async_listen(f"{DOMAIN}_data_update", self._data_update)
Expand All @@ -85,7 +85,7 @@ def _handle_coordinator_update(
if data is None:
return
self._attr_state = get_sensor_data(
data, self.entity_description.luxtronik_key.value
data, self.entity_description.luxtronik_key
)
if (
self.entity_description.on_state is True
Expand All @@ -100,3 +100,12 @@ def _handle_coordinator_update(
and self._attr_state in self.entity_description.on_states # noqa: W503
)
super()._handle_coordinator_update()

def handle_value_change(self, value, last_value):
super().handle_value_change(value, last_value)
if value == True:
self.fire_event(self.entity_description.event_id_on_true, value, last_value)
elif value == False:
self.fire_event(
self.entity_description.event_id_on_false, value, last_value
)
Loading

0 comments on commit 8ea8257

Please sign in to comment.