diff --git a/custom_components/luxtronik/binary_sensor_entities_predefined.py b/custom_components/luxtronik/binary_sensor_entities_predefined.py index d7eca7e..01151bc 100644 --- a/custom_components/luxtronik/binary_sensor_entities_predefined.py +++ b/custom_components/luxtronik/binary_sensor_entities_predefined.py @@ -10,10 +10,14 @@ LuxtronikBinarySensorEntityDescription( key=SensorKey.EVU_UNLOCKED, luxtronik_key=LC.C0031_EVU_UNLOCKED, - icon="mdi:lock", - device_class=BinarySensorDeviceClass.LOCK, + icon="mdi:transmission-tower", visibility=LV.V0121_EVU_LOCKED, ), + LuxtronikBinarySensorEntityDescription( + key=SensorKey.EVU2, + luxtronik_key=LC.C0185_EVU2, + icon="mdi:transmission-tower", + ), LuxtronikBinarySensorEntityDescription( key=SensorKey.COMPRESSOR, luxtronik_key=LC.C0044_COMPRESSOR, diff --git a/custom_components/luxtronik/const.py b/custom_components/luxtronik/const.py index 1084c70..339012b 100644 --- a/custom_components/luxtronik/const.py +++ b/custom_components/luxtronik/const.py @@ -151,6 +151,17 @@ class LuxMode(StrEnum): holidays: Final = "Holidays" +class LuxSmartGridStatus(StrEnum): + """SmartGrid status based on EVU and EVU2 inputs.""" + + locked: Final = "evu_locked" # EVU=1, EVU2=0 - Status 1 - EVU lock + reduced: Final = "reduced_operation" # EVU=0, EVU2=0 - Status 2 - Reduced operation + normal: Final = "normal_operation" # EVU=0, EVU2=1 - Status 3 - Normal operation + increased: Final = ( + "increased_operation" # EVU=1, EVU2=1 - Status 4 - Increased operation + ) + + class LuxStatus1Option(StrEnum): """LuxStatus1 option defrost etc.""" @@ -271,6 +282,13 @@ class LuxSwitchoffReason(Enum): LuxOperationMode.cooling.value: "mdi:air-conditioner", } +LUX_SMART_GRID_ICON_MAP: Final[dict[StateType | date | datetime | Decimal, str]] = { + LuxSmartGridStatus.locked.value: "mdi:cancel", # EVU lock - Blocked + LuxSmartGridStatus.reduced.value: "mdi:arrow-down-circle", # Reduced operation + LuxSmartGridStatus.normal.value: "mdi:check-circle", # Normal operation + LuxSmartGridStatus.increased.value: "mdi:arrow-up-circle", # Increased operation +} + LUX_MODELS_ALPHA_INNOTEC = ["LWP", "LWV", "MSW", "SWC", "SWP"] LUX_MODELS_NOVELAN = ["BW", "LA", "LD", "LI", "SI", "ZLW"] LUX_MODELS_OTHER = ["CB", "CI", "CN", "CS"] @@ -591,6 +609,7 @@ class LuxCalculation(StrEnum): C0180_HIGH_PRESSURE: Final = "calculations.ID_WEB_LIN_HD" C0181_LOW_PRESSURE: Final = "calculations.ID_WEB_LIN_ND" C0182_COMPRESSOR_HEATER: Final = "calculations.ID_WEB_LIN_VDH_out" + C0185_EVU2: Final = "calculations.ID_WEB_HZIO_EVU2" # C0187_CURRENT_OUTPUT: Final = "calculations.ID_WEB_SEC_Qh_Soll" # C0188_CURRENT_OUTPUT: Final = "calculations.ID_WEB_SEC_Qh_Ist" C0204_HEAT_SOURCE_INPUT_TEMPERATURE: Final = "calculations.ID_WEB_Temperatur_TWE" @@ -825,6 +844,8 @@ class SensorKey(StrEnum): ) SOLAR_PUMP_MAX_TEMPERATURE_COLLECTOR = "solar_pump_max_temperature_collector" EVU_UNLOCKED = "evu_unlocked" + EVU2 = "evu2" + SMART_GRID_STATUS = "smart_grid_status" COMPRESSOR = "compressor" COMPRESSOR2 = "compressor2" PUMP_FLOW = "pump_flow" diff --git a/custom_components/luxtronik/sensor.py b/custom_components/luxtronik/sensor.py index dad83ef..293feab 100644 --- a/custom_components/luxtronik/sensor.py +++ b/custom_components/luxtronik/sensor.py @@ -27,9 +27,12 @@ DeviceKey, LuxCalculation as LC, LuxOperationMode, + LuxParameter as LP, + LuxSmartGridStatus, LuxStatus1Option, LuxStatus3Option, SensorAttrKey as SA, + SensorKey, ) from .coordinator import LuxtronikCoordinator, LuxtronikCoordinatorData from .evu_helper import LuxtronikEVUTracker @@ -84,7 +87,19 @@ async def async_setup_entry( for description in SENSORS_STATUS if ( coordinator.entity_active(description) - and key_exists(coordinator.data, description.luxtronik_key) + and ( + description.luxtronik_key == LC.UNSET + or key_exists(coordinator.data, description.luxtronik_key) + ) + and ( + # For SmartGrid status sensor, check if required parameters exist + description.key != SensorKey.SMART_GRID_STATUS + or ( + key_exists(coordinator.data, LP.P1030_SMART_GRID_SWITCH) + and key_exists(coordinator.data, LC.C0031_EVU_UNLOCKED) + and key_exists(coordinator.data, LC.C0185_EVU2) + ) + ) ) ], True, @@ -208,7 +223,13 @@ def _handle_coordinator_update( self, data: LuxtronikCoordinatorData | None = None ) -> None: """Handle updated data from the coordinator.""" + if self.entity_description.key == SensorKey.SMART_GRID_STATUS: + self._update_smart_grid_status() + return + + # For normal status sensors, use the parent's update logic super()._handle_coordinator_update(data) + self._evu_tracker.update(self._attr_native_value) if self._attr_native_value is None or self._last_state is None: @@ -310,6 +331,52 @@ def _build_status_text(self) -> str: else f"{line_1} {line_2} {status_time}." ) + def _update_smart_grid_status(self) -> None: + """Calculate and update SmartGrid status based on EVU and EVU2 inputs.""" + from .const import LuxParameter as LP + + # Check if SmartGrid is enabled (P1030) + smartgrid_enabled = self._get_value(LP.P1030_SMART_GRID_SWITCH) + + # If SmartGrid is disabled, set sensor to unavailable + if not smartgrid_enabled or smartgrid_enabled in [False, 0, "false", "False"]: + self._attr_available = False + self._attr_native_value = None + else: + self._attr_available = True + + evu = self._get_value(LC.C0031_EVU_UNLOCKED) + evu2 = self._get_value(LC.C0185_EVU2) + + # Convert to boolean (handle True/False/1/0/"true"/"false") + evu_on = evu in [True, 1, "true", "True"] + evu2_on = evu2 in [True, 1, "true", "True"] + + # Determine SmartGrid status based on EVU and EVU2 inputs + # EVU=0, EVU2=0 → Status 2 (reduced operation) + # EVU=1, EVU2=0 → Status 1 (EVU locked) + # EVU=0, EVU2=1 → Status 3 (normal operation) + # EVU=1, EVU2=1 → Status 4 (increased operation) + if evu_on and not evu2_on: + self._attr_native_value = LuxSmartGridStatus.locked.value # Status 1 + elif not evu_on and not evu2_on: + self._attr_native_value = LuxSmartGridStatus.reduced.value # Status 2 + elif not evu_on and evu2_on: + self._attr_native_value = LuxSmartGridStatus.normal.value # Status 3 + else: # evu_on and evu2_on + self._attr_native_value = LuxSmartGridStatus.increased.value # Status 4 + + # Set icon based on current state + descr = self.entity_description + if descr.icon_by_state and self._attr_native_value in descr.icon_by_state: + self._attr_icon = descr.icon_by_state.get(self._attr_native_value) + elif descr.icon: + self._attr_icon = descr.icon + + # Don't call super() to avoid setting value to None (luxtronik_key=UNSET) + self._enrich_extra_attributes() + self.async_write_ha_state() + class LuxtronikIndexSensor(LuxtronikSensorEntity, SensorEntity): _min_index = 0 diff --git a/custom_components/luxtronik/sensor_entities_predefined.py b/custom_components/luxtronik/sensor_entities_predefined.py index 8214664..b22c13f 100644 --- a/custom_components/luxtronik/sensor_entities_predefined.py +++ b/custom_components/luxtronik/sensor_entities_predefined.py @@ -14,6 +14,7 @@ from homeassistant.helpers.entity import EntityCategory from .const import ( + LUX_SMART_GRID_ICON_MAP, LUX_STATE_ICON_MAP, SECOND_TO_HOUR_FACTOR, UPDATE_INTERVAL_NORMAL, @@ -23,6 +24,7 @@ LuxCalculation as LC, LuxOperationMode, LuxParameter as LP, + LuxSmartGridStatus, LuxStatus1Option, LuxStatus3Option, LuxSwitchoffReason, @@ -56,6 +58,14 @@ options=[e.value for e in LuxOperationMode], update_interval=UPDATE_INTERVAL_NORMAL, ), + descr( + key=SensorKey.SMART_GRID_STATUS, + luxtronik_key=LC.UNSET, # Calculated from EVU and EVU2 inputs + icon_by_state=LUX_SMART_GRID_ICON_MAP, + device_class=SensorDeviceClass.ENUM, + options=[e.value for e in LuxSmartGridStatus], + update_interval=UPDATE_INTERVAL_NORMAL, + ), ] SENSORS_INDEX: list[descr] = [ diff --git a/custom_components/luxtronik/translations/cs.json b/custom_components/luxtronik/translations/cs.json index 0be7fdb..24fba5f 100644 --- a/custom_components/luxtronik/translations/cs.json +++ b/custom_components/luxtronik/translations/cs.json @@ -4,6 +4,9 @@ "evu_unlocked": { "name": "HDO" }, + "evu2": { + "name": "EVU2" + }, "compressor": { "name": "Kompresor" }, @@ -131,8 +134,7 @@ }, "thermal_power_limitation_water": { "name": "Thermal power limit: Water" - } - , + }, "thermal_power_limitation_cooling": { "name": "Thermal power limit: Cooling" }, @@ -270,6 +272,15 @@ } } }, + "smart_grid_status": { + "name": "Stav SmartGrid", + "state": { + "evu_locked": "Blokov\u00e1no EVU", + "reduced_operation": "Sn\u00ed\u017een\u00fd provoz", + "normal_operation": "Norm\u00e1ln\u00ed provoz", + "increased_operation": "Zv\u00fd\u0161en\u00fd provoz" + } + }, "error_reason": { "name": "Posledn\u00ed chyba", "state": { @@ -909,10 +920,10 @@ }, "select_devices": { "title": "Vyberte za\u0159\u00edzen\u00ed k nakonfigurov\u00e1n\u00ed", - "description": "Vyberte jedno nebo v\u00edce za\u0159\u00edzen\u00ed k p\u0159id\u00e1n\u00ed. Ji\u017e nakonfigurovan\u00e1 za\u0159\u00edzen\u00ed: {configured}", + "description": "Vyberte jedno nebo v\u00edce za\u0159\u00edzen\u00ed k p\u0159id\u00e1n\u00ed. Ji\u017e nakonfigurovan\u00e1 za\u0159\u00edzen\u00ed: {configured}", "data": { "select_device_to_configure": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nakonfigurovat" - } + } }, "options": { "description": "Nastaven\u00ed pro tepeln\u00e9 \u010derpadlo Luxtronik {name}.\nTato nastaven\u00ed lze pozd\u011bji zm\u011bnit v sekci Integrace.", diff --git a/custom_components/luxtronik/translations/de.json b/custom_components/luxtronik/translations/de.json index f1124ea..05c615c 100644 --- a/custom_components/luxtronik/translations/de.json +++ b/custom_components/luxtronik/translations/de.json @@ -4,6 +4,9 @@ "evu_unlocked": { "name": "Sperrzeit Freigabe" }, + "evu2": { + "name": "EVU2" + }, "compressor": { "name": "Verdichter" }, @@ -131,10 +134,9 @@ }, "thermal_power_limitation_water": { "name": "Therm. Leistung max: Warmwasser" - } - , + }, "thermal_power_limitation_cooling": { - "name": "Therm. Leistung max: Kühlung" + "name": "Therm. Leistung max: K\u00fchlung" }, "heating_threshold": { "name": "Heizgrenze" @@ -270,6 +272,15 @@ } } }, + "smart_grid_status": { + "name": "SmartGrid Status", + "state": { + "evu_locked": "EVU-Sperre", + "reduced_operation": "abgesenkte Betriebsweise", + "normal_operation": "Normalbetrieb", + "increased_operation": "erh\u00f6hte Betriebsweise" + } + }, "error_reason": { "name": "Letzter Fehler", "state": { @@ -917,7 +928,7 @@ "description": "W\u00e4hlen Sie ein oder mehrere Ger\u00e4te zur Konfiguration aus. Bereits konfigurierte Ger\u00e4te: {configured}", "data": { "select_device_to_configure": "W\u00e4hlen Sie das Ger\u00e4t aus, das Sie konfigurieren m\u00f6chten" - } + } }, "options": { "description": "Einstellungen f\u00fcr die Luxtronik-W\u00e4rmepumpe {name}.\nDiese Einstellungen k\u00f6nnen sp\u00e4ter unter Integrationen ge\u00e4ndert werden.", @@ -952,4 +963,4 @@ } } } -} +} \ No newline at end of file diff --git a/custom_components/luxtronik/translations/en.json b/custom_components/luxtronik/translations/en.json index 115f8f9..7cad2c2 100644 --- a/custom_components/luxtronik/translations/en.json +++ b/custom_components/luxtronik/translations/en.json @@ -4,6 +4,9 @@ "evu_unlocked": { "name": "Locktime" }, + "evu2": { + "name": "EVU2" + }, "compressor": { "name": "Compressor" }, @@ -273,6 +276,15 @@ } } }, + "smart_grid_status": { + "name": "SmartGrid Status", + "state": { + "evu_locked": "EVU locked", + "reduced_operation": "Reduced operation", + "normal_operation": "Normal operation", + "increased_operation": "Increased operation" + } + }, "error_reason": { "name": "Last error", "state": { diff --git a/custom_components/luxtronik/translations/nl.json b/custom_components/luxtronik/translations/nl.json index 2e278f7..9675c6e 100644 --- a/custom_components/luxtronik/translations/nl.json +++ b/custom_components/luxtronik/translations/nl.json @@ -4,6 +4,9 @@ "evu_unlocked": { "name": "Tijdvrijgave blokkeren" }, + "evu2": { + "name": "EVU2" + }, "compressor": { "name": "Compressor" }, @@ -267,6 +270,15 @@ } } }, + "smart_grid_status": { + "name": "SmartGrid Status", + "state": { + "evu_locked": "EVU geblokkeerd", + "reduced_operation": "Verminderde werking", + "normal_operation": "Normale werking", + "increased_operation": "Verhoogde werking" + } + }, "error_reason": { "name": "Laatste fout", "state": { diff --git a/custom_components/luxtronik/translations/pl.json b/custom_components/luxtronik/translations/pl.json index 186a5fa..6505ab9 100644 --- a/custom_components/luxtronik/translations/pl.json +++ b/custom_components/luxtronik/translations/pl.json @@ -4,6 +4,9 @@ "evu_unlocked": { "name": "Czas blokady" }, + "evu2": { + "name": "EVU2" + }, "compressor": { "name": "Kompresor" }, @@ -131,8 +134,7 @@ }, "thermal_power_limitation_water": { "name": "Thermal power limit: Water" - } - , + }, "thermal_power_limitation_cooling": { "name": "Thermal power limit: Cooling" }, @@ -261,6 +263,15 @@ } } }, + "smart_grid_status": { + "name": "Status SmartGrid", + "state": { + "evu_locked": "Zablokowane EVU", + "reduced_operation": "Praca obni\u017cona", + "normal_operation": "Praca normalna", + "increased_operation": "Praca zwi\u0119kszona" + } + }, "error_reason": { "name": "Ostatni b\u0142\u0105d", "state": { @@ -573,13 +584,13 @@ }, "date": { "away_dhw_startdate": { - "name": "Data rozpoczęcia trybu nieobecno\u015bci - ciep\u0142a woda" + "name": "Data rozpocz\u0119cia trybu nieobecno\u015bci - ciep\u0142a woda" }, "away_dhw_enddate": { "name": "Data zako\u0144czenia trybu nieobecno\u015bci - ciep\u0142a woda" }, "away_heating_startdate": { - "name": "Data rozpoczęcia trybu nieobecno\u015bci - ogrzewanie" + "name": "Data rozpocz\u0119cia trybu nieobecno\u015bci - ogrzewanie" }, "away_heating_enddate": { "name": "Data zako\u0144czenia trybu nieobecno\u015bci - ogrzewanie" @@ -592,9 +603,9 @@ "None": "Brak", "Monday": "Poniedzia\u0142ek", "Tuesday": "Wtorek", - "Wednesday": "Środa", + "Wednesday": "\u015aroda", "Thursday": "Czwartek", - "Friday": "Piątek", + "Friday": "Pi\u0105tek", "Saturday": "Sobota", "Sunday": "Niedziela" } @@ -653,7 +664,7 @@ }, "select_devices": { "title": "Wybierz urz\u0105dzenia do skonfigurowania", - "description": "Wybierz jedno lub wi\u0119cej urz\u0105dze\u0144 do dodania. Ju\u017c skonfigurowane urz\u0105dzenia: {configured}", + "description": "Wybierz jedno lub wi\u0119cej urz\u0105dze\u0144 do dodania. Ju\u017c skonfigurowane urz\u0105dzenia: {configured}", "data": { "select_device_to_configure": "Wybierz urz\u0105dzenie, kt\u00f3re chcesz skonfigurowa\u0107" }