Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
qubabos authored Oct 23, 2023
2 parents 6c51a3f + c614f4f commit cfebd52
Show file tree
Hide file tree
Showing 18 changed files with 507 additions and 247 deletions.
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: BenPru
14 changes: 14 additions & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Validate with hassfest

on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'

jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "home-assistant/actions/hassfest@master"
18 changes: 18 additions & 0 deletions .github/workflows/validate-hacs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: HACS Action

on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:

jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
61 changes: 34 additions & 27 deletions custom_components/luxtronik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,40 +102,44 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry.version = 4
hass.config_entries.async_update_entry(config_entry, data=new_data)

if config_entry.version >= 4:
# Ensure sensor prefix:
# Ensure sensor prefix:
prefix = None
ent_reg = None

def _prepare_up() -> None:
prefix = config_entry.data[CONF_HA_SENSOR_PREFIX]
ent_reg = async_get(hass)

def _up(ident: str, new_id: SK, platform: P = P.SENSOR) -> None:
entity_id = f"{platform}.{prefix}_{ident}"
new_ident = f"{platform}.{prefix}_{new_id}"
try:
ent_reg.async_update_entity(
entity_id, new_entity_id=new_ident, new_unique_id=new_ident
)
except KeyError as err:
LOGGER.info(
"Skip rename entity - Not existing: %s->%s",
entity_id,
new_ident,
exc_info=err,
)
except ValueError as err:
LOGGER.warning(
"Could not rename entity %s->%s", entity_id, new_ident, exc_info=err
)
except Exception as err:
LOGGER.error(
"Could not rename entity %s->%s", entity_id, new_ident, exc_info=err
)
def _up(ident: str, new_id: SK, platform: P = P.SENSOR) -> None:
entity_id = f"{platform}.{prefix}_{ident}"
new_ident = f"{platform}.{prefix}_{new_id}"
try:
ent_reg.async_update_entity(
entity_id, new_entity_id=new_ident, new_unique_id=new_ident
)
except KeyError as err:
LOGGER.info(
"Skip rename entity - Not existing: %s->%s",
entity_id,
new_ident,
exc_info=err,
)
except ValueError as err:
LOGGER.warning(
"Could not rename entity %s->%s", entity_id, new_ident, exc_info=err
)
except Exception as err:
LOGGER.error(
"Could not rename entity %s->%s", entity_id, new_ident, exc_info=err
)

if config_entry.version == 4:
new_data = {**config_entry.data}
config_entry.version = 5
hass.config_entries.async_update_entry(config_entry, data=new_data)

if config_entry.version == 5:
_prepare_up()
_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)
Expand Down Expand Up @@ -257,14 +261,17 @@ 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 == 6:
_up("cooling_threshold_temperature", SK.COOLING_OUTDOOR_TEMP_THRESHOLD, P.NUMBER)
_prepare_up()
_up(
"cooling_threshold_temperature", SK.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)

new_data = {**config_entry.data}
config_entry.version = 7
hass.config_entries.async_update_entry(config_entry, data=new_data)

LOGGER.info("Migration to version %s successful", config_entry.version)

return True
Expand Down
68 changes: 39 additions & 29 deletions custom_components/luxtronik/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@
# region Imports
from __future__ import annotations

from contextlib import suppress
from datetime import datetime
import locale
from typing import Any

from homeassistant.backports.enum import StrEnum
from homeassistant.components.water_heater import STATE_HEAT_PUMP
from homeassistant.const import STATE_OFF, UnitOfTemperature, UnitOfTime
from homeassistant.core import callback
from homeassistant.helpers.typing import UndefinedType
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
Expand All @@ -21,6 +18,7 @@
from .common import get_sensor_data
from .const import (
DeviceKey,
LOGGER,
LuxCalculation as LC,
LuxMode,
LuxOperationMode,
Expand All @@ -41,6 +39,11 @@ class LuxtronikEntity(CoordinatorEntity[LuxtronikCoordinator], RestoreEntity):
next_update: datetime | None = None

_attr_cache: dict[SA, Any] = {}
_entity_component_unrecorded_attributes = frozenset(
{
SA.LUXTRONIK_KEY,
}
)

def __init__(
self,
Expand All @@ -50,6 +53,7 @@ def __init__(
) -> None:
"""Init LuxtronikEntity."""
super().__init__(coordinator=coordinator)
self._device_info_ident = device_info_ident
self._attr_extra_state_attributes = {
SA.LUXTRONIK_KEY: f"{description.luxtronik_key.name[1:5]} {description.luxtronik_key.value}"
}
Expand All @@ -71,7 +75,7 @@ def __init__(
description
)
self.entity_description = description
self._attr_device_info = coordinator.device_infos[device_info_ident.value]
self._attr_device_info = coordinator.get_device(device_info_ident)

translation_key = (
description.key.value
Expand All @@ -85,33 +89,39 @@ def __init__(
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# try set locale for local formatting
with suppress(locale.Error):
ha_locale = f"{self.hass.config.language}_{self.hass.config.country}"
locale.setlocale(locale.LC_ALL, locale.normalize(ha_locale))

last_state = await self.async_get_last_state()
if last_state is None:
return
self._attr_state = last_state.state
# Force device name:
self._attr_device_info = self.coordinator.get_device(
self._device_info_ident, self.platform
)

for attr in self.entity_description.extra_attributes:
if not attr.restore_on_startup or attr.key not in last_state.attributes:
continue
self._attr_cache[attr.key] = self._restore_attr_value(
last_state.attributes[attr.key]
try:
last_state = await self.async_get_last_state()
if last_state is None:
return
self._attr_state = last_state.state

for attr in self.entity_description.extra_attributes:
if not attr.restore_on_startup or attr.key not in last_state.attributes:
continue
self._attr_cache[attr.key] = self._restore_attr_value(
last_state.attributes[attr.key]
)

last_extra_data = await self.async_get_last_extra_data()
if last_extra_data is not None:
data: dict[str, Any] = last_extra_data.as_dict()
for attr in data:
setattr(self, attr, data.get(attr))

data_updated = f"{self.entity_id}_data_updated"
async_dispatcher_connect(
self.hass, data_updated, self._schedule_immediate_update
)
except Exception as err:
LOGGER.error(
"Could not restore latest data (async_added_to_hass)",
exc_info=err,
)

last_extra_data = await self.async_get_last_extra_data()
if last_extra_data is not None:
data: dict[str, Any] = last_extra_data.as_dict()
for attr in data:
setattr(self, attr, data.get(attr))

data_updated = f"{self.entity_id}_data_updated"
async_dispatcher_connect(
self.hass, data_updated, self._schedule_immediate_update
)

def _restore_attr_value(self, value: Any | None) -> Any:
return value
Expand Down
2 changes: 1 addition & 1 deletion custom_components/luxtronik/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def state_as_number_or_none(state: State, default: float | None = None) -> float
return default if not isinstance(result, float) or result is None else result


async def _async_get_mac_address(hass: HomeAssistant, host: str) -> str | None:
async def async_get_mac_address(hass: HomeAssistant, host: str) -> str | None:
"""Get mac address from host name, IPv4 address, or IPv6 address."""
# Help mypy, which has trouble with the async_add_executor_job + partial call
mac_address: str | None
Expand Down
57 changes: 39 additions & 18 deletions custom_components/luxtronik/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,6 @@ class LuxMkTypes(Enum):
heating_cooling: Final = 4


LUX_PARAMETER_MK_SENSORS: Final = [
"parameters.ID_Einst_MK1Typ_akt",
"parameters.ID_Einst_MK2Typ_akt",
"parameters.ID_Einst_MK3Typ_akt",
]


class LuxRoomThermostatType(Enum):
"""LuxMkTypes etc."""

Expand Down Expand Up @@ -241,12 +234,13 @@ class LuxParameter(StrEnum):
P0016_HEATING_CIRCUIT2_CURVE_NIGHT_TEMPERATURE: Final = (
"parameters.ID_Einst_HzMK1ABS_akt" # 0
)
# P0036_SECOND_HEAT_GENERATOR: Final = "parameters.ID_Einst_ZWE1Art_akt" # = 1 --> Heating and domestic water - Is second heat generator activated 1=electrical heater
P0042_MIXING_CIRCUIT1_TYPE: Final = "parameters.ID_Einst_MK1Typ_akt"
P0047_DHW_THERMAL_DESINFECTION_TARGET: Final = "parameters.ID_Einst_LGST_akt"
P0049_PUMP_OPTIMIZATION: Final = "parameters.ID_Einst_Popt_akt"
P0033_ROOM_THERMOSTAT_TYPE: Final = "parameters.ID_Einst_RFVEinb_akt"
# P0033_ROOM_THERMOSTAT_TYPE: Final = "parameters.ID_Einst_RFVEinb_akt" # != 0 --> Has_Room_Temp
P0074_DHW_HYSTERESIS: Final = "parameters.ID_Einst_BWS_Hyst_akt"
P0085_DHW_CHARGING_PUMP: Final = "parameters.ID_Einst_BWZIP_akt"
P0085_DHW_CHARGING_PUMP: Final = "parameters.ID_Einst_BWZIP_akt" # has_domestic_water_circulation_pump int() != 1
P0088_HEATING_HYSTERESIS: Final = "parameters.ID_Einst_HRHyst_akt"
P0089_HEATING_MAX_FLOW_OUT_INCREASE_TEMPERATURE: Final = (
"parameters.ID_Einst_TRErhmax_akt"
Expand Down Expand Up @@ -295,13 +289,30 @@ class LuxParameter(StrEnum):
"parameters.ID_Einst_Effizienzpumpe_Minimal_akt"
)
P0869_EFFICIENCY_PUMP: Final = "parameters.ID_Einst_Effizienzpumpe_akt"
P0870_AMOUNT_COUNTER_ACTIVE: Final = "parameters.ID_Einst_Waermemenge_akt"
# P0870_AMOUNT_COUNTER_ACTIVE: Final = "parameters.ID_Einst_Waermemenge_akt"
P0874_SERIAL_NUMBER: Final = "parameters.ID_WP_SerienNummer_DATUM"
P0875_SERIAL_NUMBER_MODEL: Final = "parameters.ID_WP_SerienNummer_HEX"

# "852 ID_Waermemenge_Seit ": "2566896",
# "853 ID_Waermemenge_WQ ": "0",
# "854 ID_Waermemenge_Hz ": "3317260",
# "855 ID_Waermemenge_WQ_ges ": "0",
# "878 ID_Waermemenge_BW ": "448200",
# "879 ID_Waermemenge_SW ": "0",
# "880 ID_Waermemenge_Datum ": "1483648906", <-- Unix timestamp! 5.1.2017

# "1059 ID_Waermemenge_ZWE ": "0",
# "1060 ID_Waermemenge_Reset ": "535051",
# "1061 ID_Waermemenge_Reset_2 ": "0",

# Calc
# "154 ID_WEB_WMZ_Seit ": "25668.9",

P0882_SOLAR_OPERATION_HOURS: Final = "parameters.ID_BSTD_Solar"
P0883_SOLAR_PUMP_MAX_TEMPERATURE_COLLECTOR: Final = (
"parameters.ID_Einst_TDC_Koll_Max_akt"
)
# P0894_VENTILATION_MODE: Final = "parameters.ID_Einst_BA_Lueftung_akt" # "Automatic", "Party", "Holidays", "Off"
P0966_COOLING_TARGET_TEMPERATURE_MK3: Final = "parameters.ID_Sollwert_KuCft3_akt"
P0979_HEATING_MIN_FLOW_OUT_TEMPERATURE: Final = (
"parameters.ID_Einst_Minimale_Ruecklaufsolltemperatur"
Expand All @@ -321,10 +332,19 @@ class LuxParameter(StrEnum):
)
P1136_HEAT_ENERGY_INPUT: Final = "parameters.Unknown_Parameter_1136"
P1137_DHW_ENERGY_INPUT: Final = "parameters.Unknown_Parameter_1137"
# ? P1138_SWIMMING_POOL_ENERGY_INPUT: Final = "parameters.Unknown_Parameter_1138" -->
# ? P1139_COOLING_ENERGY_INPUT: Final = "parameters.Unknown_Parameter_1139"
# ? P1140_SECOND_HEAT_SOURCE_DHW_ENERGY_INPUT: Final = "parameters.Unknown_Parameter_1140"


# endregion Lux parameters

LUX_PARAMETER_MK_SENSORS: Final = [
LuxParameter.P0042_MIXING_CIRCUIT1_TYPE,
LuxParameter.P0130_MIXING_CIRCUIT2_TYPE,
LuxParameter.P0780_MIXING_CIRCUIT3_TYPE,
]


# region Lux calculations
class LuxCalculation(StrEnum):
Expand All @@ -346,18 +366,24 @@ class LuxCalculation(StrEnum):
C0027_SOLAR_BUFFER_TEMPERATURE: Final = "calculations.ID_WEB_Temperatur_TSS"
C0029_DEFROST_END_FLOW_OKAY: Final = "calculations.ID_WEB_ASDin"
C0031_EVU_UNLOCKED: Final = "calculations.ID_WEB_EVUin"
# C0032_HIGH_PRESSURE_OKAY: Final = "calculations.ID_WEB_HDin" # True/False -> Hochdruck OK
C0034_MOTOR_PROTECTION: Final = "calculations.ID_WEB_MOTin"
C0037_DEFROST_VALVE: Final = "calculations.ID_WEB_AVout"
C0038_DHW_RECIRCULATION_PUMP: Final = "calculations.ID_WEB_BUPout"
C0039_CIRCULATION_PUMP_HEATING: Final = "calculations.ID_WEB_HUPout"
# C0040_MIXER1_OPENED: Final = "calculations.ID_WEB_MA1out" # True/False -> Mischer 1 auf
# C0041_MIXER1_CLOSED: Final = "calculations.ID_WEB_MZ1out" # True/False -> Mischer 1 zu
C0043_PUMP_FLOW: Final = "calculations.ID_WEB_VBOout"
C0044_COMPRESSOR: Final = "calculations.ID_WEB_VD1out"
C0045_COMPRESSOR2: Final = "calculations.ID_WEB_VD2out"
C0046_DHW_CIRCULATION_PUMP: Final = "calculations.ID_WEB_ZIPout"
C0047_ADDITIONAL_CIRCULATION_PUMP: Final = "calculations.ID_WEB_ZUPout"
C0048_ADDITIONAL_HEAT_GENERATOR: Final = "calculations.ID_WEB_ZW1out"
C0049_DISTURBANCE_OUTPUT: Final = "calculations.ID_WEB_ZW2SSTout"
# C0051: Final = "calculations.ID_WEB_FP2out" # True/False -> FBH Umwälzpumpe 2
C0052_SOLAR_PUMP: Final = "calculations.ID_WEB_SLPout"
# C0054_MIXER2_CLOSED: Final = "calculations.ID_WEB_MZ2out" # True/False -> Mischer 2 zu
# C0055_MIXER2_OPENED: Final = "calculations.ID_WEB_MA2out" # True/False -> Mischer 2 auf
C0056_COMPRESSOR1_OPERATION_HOURS: Final = "calculations.ID_WEB_Zaehler_BetrZeitVD1"
C0057_COMPRESSOR1_IMPULSES: Final = "calculations.ID_WEB_Zaehler_BetrZeitImpVD1"
C0058_COMPRESSOR2_OPERATION_HOURS: Final = "calculations.ID_WEB_Zaehler_BetrZeitVD2"
Expand Down Expand Up @@ -385,6 +411,8 @@ class LuxCalculation(StrEnum):
C0081_FIRMWARE_VERSION: Final = "calculations.ID_WEB_SoftStand"
C0095_ERROR_TIME: Final = "calculations.ID_WEB_ERROR_Time0"
C0100_ERROR_REASON: Final = "calculations.ID_WEB_ERROR_Nr0"
# TODO: !
# C0105_ERROR_COUNTER: Final = "calculations.ID_WEB_AnzahlFehlerInSpeicher"
C0117_STATUS_LINE_1: Final = "calculations.ID_WEB_HauptMenuStatus_Zeile1"
C0118_STATUS_LINE_2: Final = "calculations.ID_WEB_HauptMenuStatus_Zeile2"
C0119_STATUS_LINE_3: Final = "calculations.ID_WEB_HauptMenuStatus_Zeile3"
Expand Down Expand Up @@ -621,6 +649,7 @@ class SensorKey(StrEnum):
COOLING_TARGET_TEMPERATURE_MK2 = "cooling_target_temperature_mk2"
COOLING_TARGET_TEMPERATURE_MK3 = "cooling_target_temperature_mk3"


# endregion Keys


Expand All @@ -638,14 +667,6 @@ class SensorAttrKey(StrEnum):
"""Luxtronik sensor attribute keys."""

LUXTRONIK_KEY = "Luxtronik_Key"
LUXTRONIK_KEY_CURRENT_ACTION = "luxtronik_key_current_action"
LUXTRONIK_KEY_TARGET_TEMPERATURE = "luxtronik_key_target_temperature"
LUXTRONIK_KEY_CORRECTION_FACTOR = "luxtronik_key_correction_factor"
LUXTRONIK_KEY_CORRECTION_TARGET = "luxtronik_key_correction_target"
LUXTRONIK_KEY_CURRENT_TEMPERATURE = "luxtronik_key_current_temperature"
LUXTRONIK_ACTION_HEATING = "luxtronik_action_heating"
LUXTRONIK_KEY_TARGET_TEMPERATURE_HIGH = "luxtronik_key_target_temperature_high"
LUXTRONIK_KEY_TARGET_TEMPERATURE_LOW = "luxtronik_key_target_temperature_low"

STATUS_TEXT = "status_text"
LAST_THERMAL_DESINFECTION = "last_thermal_desinfection"
Expand Down
Loading

0 comments on commit cfebd52

Please sign in to comment.