diff --git a/custom_components/mypyllant/calendar.py b/custom_components/mypyllant/calendar.py index 44e9bb7..a9b9df4 100644 --- a/custom_components/mypyllant/calendar.py +++ b/custom_components/mypyllant/calendar.py @@ -282,6 +282,10 @@ def time_program(self) -> ZoneTimeProgram: def name(self) -> str: return self.name_prefix + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.id_infix}_heating_calendar" + def _get_calendar_id_prefix(self): return f"zone_heating_{self.zone.index}" @@ -326,6 +330,10 @@ def time_program(self) -> ZoneTimeProgram: def name(self) -> str: return self.name_prefix + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.id_infix}_coolingg_calendar" + def _get_calendar_id_prefix(self): return f"zone_cooling_{self.zone.index}" @@ -364,6 +372,10 @@ def time_program(self) -> DHWTimeProgram: def name(self) -> str: return self.name_prefix + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.id_infix}_heating_calendar" + def _get_calendar_id_prefix(self): return f"dhw_{self.domestic_hot_water.index}" @@ -404,6 +416,10 @@ def time_program(self) -> DHWTimeProgram: def name(self) -> str: return f"Circulating Water in {self.name_prefix}" + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.id_infix}_circulation_calendar" + def _get_calendar_id_prefix(self): return f"dhw_circulation_{self.domestic_hot_water.index}" @@ -443,6 +459,10 @@ def time_program(self) -> RoomTimeProgram: def name(self) -> str: return f"{self.name_prefix} Schedule" + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.id_infix}_heating_calendar" + def _get_calendar_id_prefix(self): return f"room_{self.room.room_index}" diff --git a/custom_components/mypyllant/config_flow.py b/custom_components/mypyllant/config_flow.py index 345b8b3..f3a882c 100644 --- a/custom_components/mypyllant/config_flow.py +++ b/custom_components/mypyllant/config_flow.py @@ -53,6 +53,8 @@ DEFAULT_FETCH_EEBUS, OPTION_DEFAULT_MANUAL_COOLING_DURATION, DEFAULT_MANUAL_COOLING_DURATION, + OPTION_DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE, + DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE, ) _LOGGER = logging.getLogger(__name__) @@ -178,6 +180,13 @@ async def async_step_init( DEFAULT_TIME_PROGRAM_OVERWRITE, ), ): bool, + vol.Required( + OPTION_DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE, + default=self.config_entry.options.get( + OPTION_DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE, + DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE, + ), + ): float, vol.Required( OPTION_BRAND, default=self.config_entry.options.get( diff --git a/custom_components/mypyllant/const.py b/custom_components/mypyllant/const.py index 4731862..ab65e9a 100644 --- a/custom_components/mypyllant/const.py +++ b/custom_components/mypyllant/const.py @@ -7,6 +7,9 @@ OPTION_DEFAULT_QUICK_VETO_DURATION = "quick_veto_duration" OPTION_DEFAULT_HOLIDAY_DURATION = "holiday_duration" OPTION_DEFAULT_MANUAL_COOLING_DURATION = "manual_cooling_duration" +OPTION_DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE = ( + "dhw_legionella_protection_temperature" +) OPTION_COUNTRY = "country" OPTION_BRAND = "brand" OPTION_TIME_PROGRAM_OVERWRITE = "time_program_overwrite" @@ -17,7 +20,7 @@ OPTION_FETCH_ENERGY_MANAGEMENT = "fetch_energy_management" OPTION_FETCH_EEBUS = "fetch_eebus" DEFAULT_UPDATE_INTERVAL = 60 # in seconds -DEFAULT_UPDATE_INTERVAL_DAILY = 3600 # in seconds +DEFAULT_UPDATE_INTERVAL_DAILY = 7200 # in seconds DEFAULT_REFRESH_DELAY = 5 # in seconds DEFAULT_MANUAL_COOLING_DURATION = 30 # in days DEFAULT_COUNTRY = "germany" @@ -29,9 +32,11 @@ DEFAULT_FETCH_ENERGY_MANAGEMENT = True DEFAULT_FETCH_EEBUS = True DEFAULT_MANUAL_SETPOINT_TYPE = ZoneOperatingType.HEATING +DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE = 70.0 QUOTA_PAUSE_INTERVAL = 3 * 3600 # in seconds API_DOWN_PAUSE_INTERVAL = 15 * 60 # in seconds HVAC_MODE_COOLING_FOR_DAYS = "COOLING_FOR_DAYS" +DHW_LEGIONELLA_PROTECTION_DATETIME = "dhw_legionella_protection_datetime" SERVICE_SET_QUICK_VETO = "set_quick_veto" SERVICE_SET_MANUAL_MODE_SETPOINT = "set_manual_mode_setpoint" diff --git a/custom_components/mypyllant/datetime.py b/custom_components/mypyllant/datetime.py index 1f2f440..a9f1cf4 100644 --- a/custom_components/mypyllant/datetime.py +++ b/custom_components/mypyllant/datetime.py @@ -8,12 +8,17 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from custom_components.mypyllant.const import DOMAIN, DEFAULT_HOLIDAY_SETPOINT +from custom_components.mypyllant.const import ( + DOMAIN, + DEFAULT_HOLIDAY_SETPOINT, + DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE, +) from custom_components.mypyllant.coordinator import SystemCoordinator from custom_components.mypyllant.utils import ( HolidayEntity, EntityList, ManualCoolingEntity, + DomesticHotWaterCoordinatorEntity, ) from myPyllant.utils import get_default_holiday_dates @@ -39,6 +44,19 @@ async def async_setup_entry( sensors.append( lambda: SystemHolidayEndDateTimeEntity(index, coordinator, config) ) + for dhw_index, dhw in enumerate(system.domestic_hot_water): + if dhw.current_dhw_temperature is not None: + key = f"{DOMAIN}_{system.id}_{dhw_index}_legionella_protection_datetime" + if key not in hass.data[DOMAIN][config.entry_id]: + hass.data[DOMAIN][config.entry_id][key] = None + sensors.append( + lambda: DomesticHotWaterLegionellaProtectionDateTime( + index, + dhw_index, + coordinator, + hass.data[DOMAIN][config.entry_id][key], + ) + ) if not system.control_identifier.is_vrc700 and system.is_cooling_allowed: sensors.append( lambda: SystemManualCoolingStartDateTimeEntity( @@ -178,3 +196,49 @@ async def async_set_value(self, value: datetime) -> None: @property def unique_id(self) -> str: return f"{DOMAIN}_{self.id_infix}_manual_cooling_end_date_time" + + +class DomesticHotWaterLegionellaProtectionDateTime( + DomesticHotWaterCoordinatorEntity, DateTimeEntity +): + _attr_icon = "mdi:temperature-water" + data: datetime | None = None + + def __init__( + self, system_index: int, dhw_index: int, coordinator: SystemCoordinator, data + ): + super().__init__(system_index, dhw_index, coordinator) + self.data = data + + async def async_update(self) -> None: + """ + Save last active HVAC mode after update, so it can be restored in turn_on + """ + await super().async_update() + + if self.enabled and self.legionella_protection_active: + self.data = datetime.now(tz=self.system.timezone) + + @property + def name(self): + return f"{self.name_prefix} Legionella Protection Temperature Reached" + + @property + def legionella_protection_active(self): + return ( + self.domestic_hot_water.current_dhw_temperature + > DEFAULT_DHW_LEGIONELLA_PROTECTION_TEMPERATURE + ) + + @property + def native_value(self): + if self.legionella_protection_active: + self.data = datetime.now(tz=self.system.timezone) + return self.data + + def set_value(self, value: datetime) -> None: + self.data = value + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.id_infix}_legionella_protection_datetime" diff --git a/custom_components/mypyllant/manifest.json b/custom_components/mypyllant/manifest.json index aa13710..7c539ce 100644 --- a/custom_components/mypyllant/manifest.json +++ b/custom_components/mypyllant/manifest.json @@ -6,11 +6,12 @@ ], "config_flow": true, "documentation": "https://github.com/signalkraft/mypyllant-component#readme", + "homeassistant": "2025.1.0b0", "integration_type": "hub", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/signalkraft/mypyllant-component/issues", "requirements": [ - "myPyllant==0.8.36" + "myPyllant==0.9.0b0" ], - "version": "v0.8.22" + "version": "v0.9.0b0" } diff --git a/custom_components/mypyllant/translations/en.json b/custom_components/mypyllant/translations/en.json index a73db9a..933e2df 100644 --- a/custom_components/mypyllant/translations/en.json +++ b/custom_components/mypyllant/translations/en.json @@ -47,6 +47,7 @@ "time_program_overwrite": "Temperature controls overwrite time program instead of setting quick veto", "default_holiday_setpoint": "Default temperature setpoint for away mode", "manual_cooling_duration": "Default duration for manual cooling in days", + "dhw_legionella_protection_temperature": "Temperature above which legionella protection is considered active", "country": "Country", "brand": "Brand", "fetch_rts": "Fetch real-time statistics (not supported on every system)", diff --git a/custom_components/mypyllant/translations/nl.json b/custom_components/mypyllant/translations/nl.json new file mode 100644 index 0000000..5d27fcd --- /dev/null +++ b/custom_components/mypyllant/translations/nl.json @@ -0,0 +1,61 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam", + "country": "Land", + "brand": "Merk" + }, + "title": "Inloggegevens", + "description": "Hetzelfde als de myVAILLANT-app" + } + }, + "error": { + "login_endpoint_invalid": "Er is geen inlogmethode gevonden voor deze combinatie van merk en land", + "realm_invalid": "Voor dit merk moet een land worden geselecteerd", + "authentication_failed": "Authenticatie mislukt, controleer uw gebruikersnaam en wachtwoord en zorg ervoor dat u het juiste land en apparaatmerk heeft geselecteerd", + "unknown": "Onverwachte fout" + } + }, + "entity": { + "climate": { + "mypyllant_zone": { + "state_attributes": { + "preset_mode": { + "state": { + "system_off": "Systeem uit", + "ventilation_boost": "Ventilatieboost", + "boost": "Quick Veto", + "away": "Vakantie" + } + } + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_interval": "Seconden tussen updates (verkleint het risico op 'quota overschreden'-fouten en tijdelijke verbanningen)", + "update_interval_daily": "Seconden tussen updates van energiegegevens (verlagen van het risico op 'quota overschreden'-fouten en tijdelijke verbanningen)", + "refresh_delay": "Vertraging in seconden voordat gegevens worden vernieuwd na updates", + "quick_veto_duration": "Standaardduur in uren voor quick veto", + "holiday_duration": "Standaardduur in dagen voor de afwezigheidsmodus", + "time_program_overwrite": "Temperatuurregelaars overschrijven het tijdprogramma in plaats van de instelling quick veto", + "default_holiday_setpoint": "Standaard temperatuurinstelpunt voor afwezigheidsmodus", + "manual_cooling_duration": "Standaardduur voor handmatige koeling in dagen", + "country": "Land", + "brand": "Merk", + "fetch_rts": "Haal realtime statistieken op (niet op elk systeem ondersteund)", + "fetch_mpc": "Realtime energieverbruik ophalen (niet op elk systeem ondersteund)", + "fetch_ambisense_rooms": "Haal Ambisense kamerthermostaten op", + "fetch_energy_management": "Energiebeheergegevens ophalen", + "fetch_eebus": "Haal EEBUS-gegevens op" + } + } + } + } +} \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt index 7d00e6f..5666d5b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,8 +10,8 @@ PyYAML~=6.0.1 types-PyYAML~=6.0.12.20240311 # Need specific versions -pytest-homeassistant-custom-component==0.13.142 -myPyllant==0.8.36 +pytest-homeassistant-custom-component==0.13.200 +myPyllant==0.9.0b0 # Versions handled by pytest-homeassistant-custom-component freezegun diff --git a/tests/conftest.py b/tests/conftest.py index e67f4b3..dc2df70 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -122,6 +122,7 @@ def __init__( title="Mock Title", state=None, options={}, + discovery_keys={}, pref_disable_new_entities=None, pref_disable_polling=None, unique_id=None, @@ -137,6 +138,7 @@ def __init__( "pref_disable_new_entities": pref_disable_new_entities, "pref_disable_polling": pref_disable_polling, "options": options, + "discovery_keys": discovery_keys, "version": version, "title": title, "unique_id": unique_id, diff --git a/tests/test_init.py b/tests/test_init.py index 407522e..92aa295 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -52,7 +52,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -60,7 +60,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant): user_input=test_user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM @pytest.mark.parametrize("test_data", list_test_data())