Skip to content

Commit

Permalink
* Add dutch translation (automated!)
Browse files Browse the repository at this point in the history
* Add SwitchoffReason Sensor
  • Loading branch information
BenPru committed Oct 27, 2023
1 parent 38a4d93 commit 0ae090f
Show file tree
Hide file tree
Showing 8 changed files with 1,047 additions and 17 deletions.
3 changes: 2 additions & 1 deletion custom_components/luxtronik/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any

from homeassistant.backports.enum import StrEnum
from homeassistant.components.sensor import RestoreSensor
from homeassistant.components.water_heater import STATE_HEAT_PUMP
from homeassistant.const import STATE_OFF, UnitOfTemperature, UnitOfTime
from homeassistant.core import callback
Expand All @@ -32,7 +33,7 @@
# endregion Imports


class LuxtronikEntity(CoordinatorEntity[LuxtronikCoordinator], RestoreEntity):
class LuxtronikEntity(CoordinatorEntity[LuxtronikCoordinator], RestoreSensor):
"""Luxtronik base device."""

entity_description: LuxtronikEntityDescription
Expand Down
19 changes: 19 additions & 0 deletions custom_components/luxtronik/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,22 @@ class LuxRoomThermostatType(Enum):
# Smart: Final = 5


class LuxSwitchoffReason(Enum):
"""LuxSwitchoff reason etc."""

heatpump_error: Final = 1
system_error: Final = 2
evu_lock: Final = 3
operation_mode_second_heat_generator: Final = 4
air_defrost: Final = 5
maximal_usage_temperature: Final = 6
minimal_usage_temperature: Final = 7
lower_usage_limit: Final = 8
no_request: Final = 9
flow_rate: Final = 11 # Durchfluss
PV_max: Final = 19


LUX_STATE_ICON_MAP: Final[dict[StateType | date | datetime | Decimal, str]] = {
LuxOperationMode.heating.value: "mdi:radiator",
LuxOperationMode.domestic_water.value: "mdi:waves",
Expand Down Expand Up @@ -268,6 +284,8 @@ class LuxParameter(StrEnum):
)
P0699_HEATING_THRESHOLD: Final = "parameters.ID_Einst_Heizgrenze"
P0700_HEATING_THRESHOLD_TEMPERATURE: Final = "parameters.ID_Einst_Heizgrenze_Temp"
P0716_0720_SWITCHOFF_REASON: Final = "parameters.ID_Switchoff_file_{ID}_0" # e.g. ID_Switchoff_file_0_0 - ID_Switchoff_file_4_0
P0721_0725_SWITCHOFF_TIMESTAMP: Final = "parameters.ID_Switchoff_file_{ID}_1" # e.g. ID_Switchoff_file_0_1 - ID_Switchoff_file_4_1
# luxtronik*_heating_circuit3_curve*
P0774_HEATING_CIRCUIT3_CURVE1_TEMPERATURE: Final = (
"parameters.ID_Einst_HzMK3E_akt" # 270
Expand Down Expand Up @@ -647,6 +665,7 @@ class SensorKey(StrEnum):
COOLING_TARGET_TEMPERATURE_MK1 = "cooling_target_temperature_mk1"
COOLING_TARGET_TEMPERATURE_MK2 = "cooling_target_temperature_mk2"
COOLING_TARGET_TEMPERATURE_MK3 = "cooling_target_temperature_mk3"
SWITCHOFF_REASON = "switchoff_reason"


# endregion Keys
Expand Down
2 changes: 1 addition & 1 deletion custom_components/luxtronik/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"after_dependencies": [],
"codeowners": ["@BenPru"],
"iot_class": "local_polling",
"version": "2023.10.26",
"version": "2023.10.27",
"homeassistant": "2023.1.0",
"dhcp": [
{ "macaddress": "000E8C*" },
Expand Down
10 changes: 10 additions & 0 deletions custom_components/luxtronik/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ class LuxtronikSensorDescription(
native_precision: int | None = None


@dataclass
class LuxtronikIndexSensorDescription(
LuxtronikSensorDescription,
SensorEntityDescription,
):
"""Class describing Luxtronik index sensor entities."""

luxtronik_key_timestamp: LuxParameter | LuxCalculation = LuxParameter.UNSET


@dataclass
class LuxtronikNumberDescription(
LuxtronikEntityDescription,
Expand Down
90 changes: 75 additions & 15 deletions custom_components/luxtronik/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# region Imports
from __future__ import annotations

from datetime import date, datetime, time
from datetime import date, datetime, time, timezone
from decimal import Decimal
from typing import Any

Expand All @@ -13,7 +13,7 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from homeassistant.util.dt import utcnow, dt as dt_util

from .base import LuxtronikEntity
from .common import get_sensor_data
Expand All @@ -30,8 +30,8 @@
SensorAttrKey as SA,
)
from .coordinator import LuxtronikCoordinator, LuxtronikCoordinatorData
from .model import LuxtronikSensorDescription
from .sensor_entities_predefined import SENSORS, SENSORS_STATUS
from .model import LuxtronikIndexSensorDescription, LuxtronikSensorDescription
from .sensor_entities_predefined import SENSORS, SENSORS_INDEX, SENSORS_STATUS

# endregion Imports

Expand Down Expand Up @@ -60,6 +60,14 @@ async def async_setup_entry(
if coordinator.entity_active(description)
)

async_add_entities(
LuxtronikIndexSensor(
hass, entry, coordinator, description, description.device_key
)
for description in SENSORS_INDEX
if coordinator.entity_active(description)
)


class LuxtronikSensorEntity(LuxtronikEntity, SensorEntity):
"""Luxtronik Sensor Entity."""
Expand Down Expand Up @@ -116,7 +124,7 @@ async def _data_update(self, event):
self._handle_coordinator_update()

def _handle_coordinator_update_internal(
self, data: LuxtronikCoordinatorData | None = None
self, data: LuxtronikCoordinatorData | None = None, use_key: str | None = None
) -> None:
"""Handle updated data from the coordinator."""
if (
Expand All @@ -129,7 +137,7 @@ def _handle_coordinator_update_internal(
if data is None:
return
self._attr_native_value = get_sensor_data(
data, self.entity_description.luxtronik_key.value
data, use_key or self.entity_description.luxtronik_key.value
)

if self._attr_native_value is None:
Expand All @@ -150,10 +158,10 @@ def _handle_coordinator_update_internal(

@callback
def _handle_coordinator_update(
self, data: LuxtronikCoordinatorData | None = None
self, data: LuxtronikCoordinatorData | None = None, use_key: str | None = None
) -> None:
"""Handle updated data from the coordinator."""
self._handle_coordinator_update_internal(data)
self._handle_coordinator_update_internal(data, use_key)
super()._handle_coordinator_update()


Expand Down Expand Up @@ -228,7 +236,7 @@ def _handle_coordinator_update(
# region Workaround Luxtronik Bug
else:
# region Workaround: Inverter heater is active but not the heatpump!
# Status shows heating but status 3 = no request!
# Status shows heating but status 3 = no request!
sl1 = self._get_value(LC.C0117_STATUS_LINE_1)
sl3 = self._get_value(LC.C0119_STATUS_LINE_3)
add_circ_pump = self._get_value(LC.C0047_ADDITIONAL_CIRCULATION_PUMP)
Expand All @@ -248,13 +256,13 @@ def _handle_coordinator_update(
# ignore pump forerun
self._attr_native_value = LuxOperationMode.no_request.value
# endregion Workaround: Inverter heater is active but not the heatpump!

# region Workaround Thermal desinfection with heatpump running
if sl3 == LuxStatus3Option.thermal_desinfection:
# map thermal desinfection to Domestic Water iso Heating
self._attr_native_value = LuxOperationMode.domestic_water.value
# endregion Workaround Thermal desinfection with heatpump running

# region Workaround Thermal desinfection with (only) using 2nd heatsource
s3_workaround: list[str | None] = [
LuxStatus3Option.no_request,
Expand All @@ -264,10 +272,10 @@ def _handle_coordinator_update(
DHW_recirculation = self._get_value(LC.C0038_DHW_RECIRCULATION_PUMP)
AddHeat = self._get_value(LC.C0048_ADDITIONAL_HEAT_GENERATOR)
if AddHeat and DHW_recirculation:
# more fixes to detect thermal desinfection sequences
# more fixes to detect thermal desinfection sequences
self._attr_native_value = LuxOperationMode.domestic_water.value
# endregion Workaround Thermal desinfection with (only) using 2nd heatsource

# region Workaround Detect passive cooling operation mode
if self._attr_native_value == LuxOperationMode.no_request.value:
# detect passive cooling
Expand All @@ -278,9 +286,9 @@ def _handle_coordinator_update(
T_heat_out = self._get_value(LC.C0020_HEAT_SOURCE_OUTPUT_TEMPERATURE)
Flow_WQ = self._get_value(LC.C0173_HEAT_SOURCE_FLOW_RATE)
if (T_out > T_in) and (T_heat_out > T_heat_in) and (Flow_WQ > 0):
#LOGGER.info(f"Cooling mode detected!!!")
# LOGGER.info(f"Cooling mode detected!!!")
self._attr_native_value = LuxOperationMode.cooling.value
# endregion Workaround Detect passive cooling operation mode
# endregion Workaround Detect passive cooling operation mode
# endregion Workaround Luxtronik Bug

self._last_state = self._attr_native_value
Expand Down Expand Up @@ -402,3 +410,55 @@ def _restore_attr_value(self, value: Any | None) -> Any:
return time.min
vals = str(value).split(":")
return time(int(vals[0]), int(vals[1]))


class LuxtronikIndexSensor(LuxtronikSensorEntity, SensorEntity):
_min_index = 0
_max_index = 4

entity_description: LuxtronikIndexSensorDescription

@callback
def _handle_coordinator_update(
self, data: LuxtronikCoordinatorData | None = None
) -> None:
"""Handle updated data from the coordinator."""

values = dict()
for i in range(self._min_index, self._max_index + 1):
luxtronik_key_timestamp = str(
self.entity_description.luxtronik_key_timestamp
).format(ID=i)
luxtronik_key = str(self.entity_description.luxtronik_key).format(ID=i)
key = self.coordinator.get_value(luxtronik_key_timestamp)
values[key] = self.coordinator.get_value(luxtronik_key)

values = dict(sorted(values.items()))
attr = self._attr_extra_state_attributes

item = values.popitem()
self._attr_native_value = attr[SA.CODE] = item[1]
attr[SA.TIMESTAMP] = self.format_time(item[0])

i = 1
while len(values) > 0:
item = values.popitem()
attr[SA.CODE + f"_{i}"] = item[1]
attr[SA.TIMESTAMP + f"_{i}"] = self.format_time(item[0])
i += 1

super()._handle_coordinator_update(
data, self.entity_description.luxtronik_key.format(ID=0)
)

def format_time(self, value_timestamp: int | None) -> datetime | None:
if value_timestamp is not None and isinstance(value_timestamp, int):
value_timestamp = datetime.fromtimestamp(value_timestamp, timezone.utc)
if (
value_timestamp is not None
and isinstance(value_timestamp, datetime)
and value_timestamp.tzinfo is None
):
time_zone = dt_util.get_time_zone(self.hass.config.time_zone)
value_timestamp = value_timestamp.replace(tzinfo=time_zone)
return value_timestamp
14 changes: 14 additions & 0 deletions custom_components/luxtronik/sensor_entities_predefined.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
LuxParameter as LP,
LuxStatus1Option,
LuxStatus3Option,
LuxSwitchoffReason,
LuxVisibility as LV,
SensorAttrFormat,
SensorAttrKey as SA,
Expand All @@ -34,6 +35,7 @@
from .model import (
LuxtronikEntityAttributeDescription as attr,
LuxtronikSensorDescription as descr,
LuxtronikIndexSensorDescription as descr_index,
)

# endregion Imports
Expand All @@ -55,6 +57,18 @@
),
]

SENSORS_INDEX: list[descr] = [
descr_index(
key=SensorKey.SWITCHOFF_REASON,
luxtronik_key=LP.P0716_0720_SWITCHOFF_REASON,
luxtronik_key_timestamp=LP.P0721_0725_SWITCHOFF_TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:electric-switch",
device_class=SensorDeviceClass.ENUM,
options=[e.value for e in LuxSwitchoffReason],
),
]

SENSORS: list[descr] = [
# region Main heatpump
descr(
Expand Down
85 changes: 85 additions & 0 deletions custom_components/luxtronik/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,91 @@
"9": "Keine Anforderung",
"11": "Durchfluss-W\u00e4rmequelle",
"-1": "Unbekannte Abschaltung"
},
"state_attributes": {
"timestamp": {
"name": "Zeitpunkt"
},
"code_1": {
"name": "Vorletze Abschaltung",
"state": {
"0": "W\u00e4rmepumpe St\u00f6rung",
"1": "Anlagen St\u00f6rung",
"2": "Betriebsart Zweiter W\u00e4rmeerzeuger",
"3": "EVU-Sperre",
"4": "",
"5": "Lauftabtau (nur LW-Ger\u00e4te)",
"6": "Temperatur Einsatzgrenze maximal",
"7": "Temperatur Einsatzgrenze minimal",
"8": "Untere Einsatzgrenze",
"9": "Keine Anforderung",
"11": "Durchfluss-W\u00e4rmequelle",
"-1": "Unbekannte Abschaltung"
}
},
"timestamp_1": {
"name": "Vorletzter Zeitpunkt"
},
"code_2": {
"name": "Vorvorletze Abschaltung",
"state": {
"0": "W\u00e4rmepumpe St\u00f6rung",
"1": "Anlagen St\u00f6rung",
"2": "Betriebsart Zweiter W\u00e4rmeerzeuger",
"3": "EVU-Sperre",
"4": "",
"5": "Lauftabtau (nur LW-Ger\u00e4te)",
"6": "Temperatur Einsatzgrenze maximal",
"7": "Temperatur Einsatzgrenze minimal",
"8": "Untere Einsatzgrenze",
"9": "Keine Anforderung",
"11": "Durchfluss-W\u00e4rmequelle",
"-1": "Unbekannte Abschaltung"
}
},
"timestamp_2": {
"name": "Vorvorletzter Zeitpunkt"
},
"code_3": {
"name": "Viertletzte Abschaltung",
"state": {
"0": "W\u00e4rmepumpe St\u00f6rung",
"1": "Anlagen St\u00f6rung",
"2": "Betriebsart Zweiter W\u00e4rmeerzeuger",
"3": "EVU-Sperre",
"4": "",
"5": "Lauftabtau (nur LW-Ger\u00e4te)",
"6": "Temperatur Einsatzgrenze maximal",
"7": "Temperatur Einsatzgrenze minimal",
"8": "Untere Einsatzgrenze",
"9": "Keine Anforderung",
"11": "Durchfluss-W\u00e4rmequelle",
"-1": "Unbekannte Abschaltung"
}
},
"timestamp_3": {
"name": "Viertletzter Zeitpunkt"
},
"code_4": {
"name": "F\u00fcnftletzte Abschaltung",
"state": {
"0": "W\u00e4rmepumpe St\u00f6rung",
"1": "Anlagen St\u00f6rung",
"2": "Betriebsart Zweiter W\u00e4rmeerzeuger",
"3": "EVU-Sperre",
"4": "",
"5": "Lauftabtau (nur LW-Ger\u00e4te)",
"6": "Temperatur Einsatzgrenze maximal",
"7": "Temperatur Einsatzgrenze minimal",
"8": "Untere Einsatzgrenze",
"9": "Keine Anforderung",
"11": "Durchfluss-W\u00e4rmequelle",
"-1": "Unbekannte Abschaltung"
}
},
"timestamp_4": {
"name": "F\u00fcnftletzter Zeitpunkt"
}
}
},
"status_line_1": {
Expand Down
Loading

0 comments on commit 0ae090f

Please sign in to comment.