diff --git a/README.md b/README.md index c7041d4..95440b7 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Changing temperature while ... Modes mapping: - `AUTO` -> `HVAC_MODE_AUTO` & `PRESET_COMFORT` - `OFF` -> `HVAC_MODE_OFF` & no preset -- `QUICK_VETO` -> no hvac & `PRESET_QUICK_VETO` (custom) +- `QUICK_VETO` -> hvac depends on state & `PRESET_QUICK_VETO` (custom) - `QM_SYSTEM_OFF` -> `HVAC_MODE_OFF` & `PRESET_SYSTEM_OFF` (custom) - `HOLIDAY` -> `HVAC_MODE_OFF` & `PRESET_HOLIDAY` (custom) - `MANUAL` -> no hvac & `PRESET_MANUAL` (custom) @@ -98,21 +98,49 @@ On **zone** climate: Modes mapping: -| Vaillant Mode | HA Mode | -| ------------- |-------- | -| AUTO | `HVAC_MODE_AUTO` & `PRESET_COMFORT` | -| DAY | no hvac & `PRESET_DAY` (custom) | -| NIGHT | no hvac & `PRESET_SLEEP` | -| OFF | `HVAC_MODE_OFF` & no preset | -| ON (= cooling ON) | no hvac & `PRESET_COOLING_ON` (custom) | -| QUICK_VETO | no hvac & `PRESET_QUICK_VETO` (custom) | -| QM_ONE_DAY_AT_HOME | HVAC_MODE_AUTO & `PRESET_HOME` | -| QM_PARTY | no hvac & `PRESET_PARTY` (custom) | -| QM_VENTILATION_BOOST | `HVAC_MODE_FAN_ONLY` & no preset | -| QM_ONE_DAY_AWAY | `HVAC_MODE_OFF` & `PRESET_AWAY` | -| QM_SYSTEM_OFF | `HVAC_MODE_OFF` & `PRESET_SYSTEM_OFF` (custom) | -| HOLIDAY | `HVAC_MODE_OFF` & `PRESET_HOLIDAY` (custom) | -| QM_COOLING_FOR_X_DAYS | no hvac & `PRESET_COOLING_FOR_X_DAYS` | +| Vaillant Mode | HA Mode | +| ------------- |-----------------------------------------------------| +| AUTO | `HVAC_MODE_AUTO` & `PRESET_COMFORT` | +| DAY | no hvac & `PRESET_DAY` (custom) | +| NIGHT | no hvac & `PRESET_SLEEP` | +| OFF | `HVAC_MODE_OFF` & no preset | +| ON (= cooling ON) | no hvac & `PRESET_COOLING_ON` (custom) | +| QUICK_VETO | depends on the state & `PRESET_QUICK_VETO` (custom) | +| QM_ONE_DAY_AT_HOME | HVAC_MODE_AUTO & `PRESET_HOME` | +| QM_PARTY | no hvac & `PRESET_PARTY` (custom) | +| QM_VENTILATION_BOOST | `HVAC_MODE_FAN_ONLY` & no preset | +| QM_ONE_DAY_AWAY | `HVAC_MODE_OFF` & `PRESET_AWAY` | +| QM_SYSTEM_OFF | `HVAC_MODE_OFF` & `PRESET_SYSTEM_OFF` (custom) | +| HOLIDAY | `HVAC_MODE_OFF` & `PRESET_HOLIDAY` (custom) | +| QM_COOLING_FOR_X_DAYS | no hvac & `PRESET_COOLING_FOR_X_DAYS` | + +### DHW climate + +| Vaillant Mode | HA HVAC | HA preset | +|-----------------------------|---------|-------------------| +| AUTO | AUTO | PRESET_COMFORT | +| OFF | OFF | PRESET_NONE | +| HOLIDAY (quick mode) | OFF | PRESET_AWAY | +| ONE_DAY_AWAY (quick mode) | OFF | PRESET_AWAY | +| SYSTEM_OFF (quick mode) | OFF | PRESET_SYSTEM_OFF | +| HOTWATER_BOOST (quick mode) | HEAT | PRESET_BOOST | +| PARTY (quick mode) | OFF | PRESET_HOME | +| ON | HEAT | PRESET_NONE | + +#### Available HVAC mode + +| HVAC mode | Multimatic mode | +|-----------|-----------------| +| AUTO | AUTO | +| OFF | OFF | +| HEAT | ON | + +#### Available preset mode + +| preset mode | Multimatic mode | +|----------------|-----------------------------| +| PRESET_COMFORT | AUTO | +| PRESET_BOOST | HOTWATER_BOOST (quick mode) | --- Buy Me A Coffee diff --git a/custom_components/multimatic/climate.py b/custom_components/multimatic/climate.py index e94348a..5372edd 100644 --- a/custom_components/multimatic/climate.py +++ b/custom_components/multimatic/climate.py @@ -10,6 +10,7 @@ ActiveFunction, ActiveMode, Component, + HotWater, Mode, OperatingModes, QuickModes, @@ -20,6 +21,7 @@ from homeassistant.components.climate import ( DOMAIN, PRESET_AWAY, + PRESET_BOOST, PRESET_COMFORT, PRESET_HOME, PRESET_NONE, @@ -40,6 +42,7 @@ from .const import ( CONF_APPLICATION, DEFAULT_QUICK_VETO_DURATION, + DHW, DOMAIN as MULTIMATIC, PRESET_COOLING_FOR_X_DAYS, PRESET_COOLING_ON, @@ -75,6 +78,8 @@ async def async_setup_entry( climates: list[MultimaticClimate] = [] zones_coo = get_coordinator(hass, ZONES, entry.entry_id) rooms_coo = get_coordinator(hass, ROOMS, entry.entry_id) + dhw_coo = get_coordinator(hass, DHW, entry.entry_id) + ventilation_coo = get_coordinator(hass, VENTILATION, entry.entry_id) system_application = SENSO if entry.data[CONF_APPLICATION] == SENSO else MULTIMATIC @@ -92,6 +97,9 @@ async def async_setup_entry( for room in rooms_coo.data: climates.append(RoomClimate(rooms_coo, zones_coo, room, rbr_zone)) + if dhw_coo.data: + climates.append(DHWClimate(dhw_coo)) + _LOGGER.info("Adding %s climate entities", len(climates)) async_add_entities(climates) @@ -121,6 +129,8 @@ def __init__( """Initialize entity.""" super().__init__(coordinator, DOMAIN, comp_id) self._comp_id = comp_id + self._supported_hvac = list(self._ha_mode().keys()) + self._supported_presets = list(self._ha_preset().keys()) async def set_quick_veto(self, **kwargs): """Set quick veto, called by service.""" @@ -142,6 +152,18 @@ def active_mode(self) -> ActiveMode: def component(self) -> Component: """Return the room or the zone.""" + @abstractmethod + def _ha_mode(self): + pass + + @abstractmethod + def _multimatic_mode(self): + pass + + @abstractmethod + def _ha_preset(self): + pass + @property def available(self) -> bool: """Return True if entity is available.""" @@ -217,6 +239,39 @@ def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return None + @property + def hvac_modes(self) -> list[HVACMode]: + """Return the list of available hvac operation modes.""" + return self._supported_hvac + + @property + def preset_modes(self) -> list[str] | None: + """Return a list of available preset modes. + + Requires SUPPORT_PRESET_MODE. + """ + + mapping = self._multimatic_mode().get(self.active_mode.current) + if ( + mapping is not None + and mapping[1] is not None + and mapping[1] not in self._supported_presets + ): + return self._supported_presets + [mapping[1]] + return self._supported_presets + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., home, away, temp.""" + return self._multimatic_mode()[self.active_mode.current][1] + + @property + def supported_features(self) -> ClimateEntityFeature: + """Return the list of supported features.""" + return ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ) + class RoomClimate(MultimaticClimate): """Climate for a room.""" @@ -248,10 +303,17 @@ def __init__( super().__init__(coordinator, room.name) self._zone_id = zone.id if zone else None self._room_id = room.id - self._supported_hvac = list(RoomClimate._HA_MODE_TO_MULTIMATIC.keys()) - self._supported_presets = list(RoomClimate._HA_PRESET_TO_MULTIMATIC.keys()) self._zone_coo = zone_coo + def _ha_mode(self): + return RoomClimate._HA_MODE_TO_MULTIMATIC + + def _multimatic_mode(self): + return RoomClimate._MULTIMATIC_TO_HA + + def _ha_preset(self): + return RoomClimate._HA_PRESET_TO_MULTIMATIC + @property def device_info(self) -> DeviceInfo | None: """Return device specific attributes.""" @@ -275,26 +337,15 @@ def hvac_mode(self) -> HVACMode: """Get the hvac mode based on multimatic mode.""" hvac_mode = RoomClimate._MULTIMATIC_TO_HA[self.active_mode.current][0] if not hvac_mode: - if ( - self.active_mode.current - in (OperatingModes.MANUAL, OperatingModes.QUICK_VETO) - and self.hvac_action == HVACAction.HEATING + if self.active_mode.current in ( + OperatingModes.MANUAL, + OperatingModes.QUICK_VETO, ): - return HVACMode.HEAT + if self.hvac_action == HVACAction.HEATING: + return HVACMode.HEAT + return HVACMode.OFF return hvac_mode - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available hvac operation modes.""" - return self._supported_hvac - - @property - def supported_features(self) -> ClimateEntityFeature: - """Return the list of supported features.""" - return ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - ) - @property def min_temp(self) -> float: """Return the minimum temperature.""" @@ -325,24 +376,6 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: mode = RoomClimate._HA_MODE_TO_MULTIMATIC[hvac_mode] await self.coordinator.api.set_room_operating_mode(self, mode) - @property - def preset_mode(self) -> str | None: - """Return the current preset mode, e.g., home, away, temp. - - Requires SUPPORT_PRESET_MODE. - """ - return RoomClimate._MULTIMATIC_TO_HA[self.active_mode.current][1] - - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes. - - Requires SUPPORT_PRESET_MODE. - """ - if self.active_mode.current == OperatingModes.QUICK_VETO: - return self._supported_presets + [PRESET_QUICK_VETO] - return self._supported_presets - async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" mode = RoomClimate._HA_PRESET_TO_MULTIMATIC[preset_mode] @@ -387,9 +420,6 @@ def __init__( """Initialize entity.""" super().__init__(coordinator, zone.id) - self._supported_hvac = list(self._ha_mode().keys()) - self._supported_presets = list(self._ha_preset().keys()) - if not zone.cooling: self._supported_presets.remove(PRESET_COOLING_ON) self._supported_presets.remove(PRESET_COOLING_FOR_X_DAYS) @@ -400,18 +430,6 @@ def __init__( self._zone_id = zone.id - @abstractmethod - def _ha_mode(self): - pass - - @abstractmethod - def _multimatic_mode(self): - pass - - @abstractmethod - def _ha_preset(self): - pass - @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" @@ -451,18 +469,6 @@ def hvac_mode(self) -> HVACMode: return HVACMode.COOL return hvac_mode if hvac_mode else HVACMode.OFF - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available hvac operation modes.""" - return self._supported_hvac - - @property - def supported_features(self) -> ClimateEntityFeature: - """Return the list of supported features.""" - return ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - ) - @property def min_temp(self) -> float: """Return the minimum temperature.""" @@ -473,11 +479,6 @@ def max_temp(self) -> float: """Return the maximum temperature.""" return Zone.MAX_TARGET_TEMP - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self.active_mode.target - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) @@ -501,18 +502,6 @@ def hvac_action(self) -> HVACAction | None: """ return _FUNCTION_TO_HVAC_ACTION.get(self.component.active_function) - @property - def preset_mode(self) -> str | None: - """Return the current preset mode, e.g., home, away, temp.""" - return self._multimatic_mode()[self.active_mode.current][1] - - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes.""" - if self.active_mode.current == OperatingModes.QUICK_VETO: - return self._supported_presets + [PRESET_QUICK_VETO] - return self._supported_presets - async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" mode = self._ha_preset()[preset_mode] @@ -612,3 +601,100 @@ def _multimatic_mode(self): def _ha_preset(self): return ZoneClimateSenso._HA_PRESET_TO_SENSO + + +class DHWClimate(MultimaticClimate): + """Climate entity representing DHW.""" + + _HA_MODE_TO_MULTIMATIC = { + HVACMode.OFF: OperatingModes.OFF, + HVACMode.HEAT: OperatingModes.ON, + HVACMode.AUTO: OperatingModes.AUTO, + } + + _HA_PRESET_TO_MULTIMATIC = { + PRESET_COMFORT: OperatingModes.AUTO, + PRESET_BOOST: QuickModes.HOTWATER_BOOST, + } + + _MULTIMATIC_TO_HA: dict[Mode, list] = { + OperatingModes.OFF: [HVACMode.OFF, PRESET_NONE], + QuickModes.HOLIDAY: [HVACMode.OFF, PRESET_AWAY], + QuickModes.ONE_DAY_AWAY: [HVACMode.OFF, PRESET_AWAY], + QuickModes.SYSTEM_OFF: [HVACMode.OFF, PRESET_SYSTEM_OFF], + QuickModes.HOTWATER_BOOST: [HVACMode.HEAT, PRESET_BOOST], + QuickModes.PARTY: [HVACMode.OFF, PRESET_HOME], + OperatingModes.ON: [HVACMode.HEAT, PRESET_NONE], + OperatingModes.AUTO: [HVACMode.AUTO, PRESET_COMFORT], + } + + def __init__(self, coordinator: MultimaticCoordinator) -> None: + """Initialize entity.""" + super().__init__(coordinator, coordinator.data.hotwater.id) + + async def set_quick_veto(self, **kwargs): + """Set quick veto, called by service.""" + _LOGGER.info("Cannot set quick veto for hotwater") + + async def remove_quick_veto(self, **kwargs): + """Remove quick veto, called by service.""" + _LOGGER.info("Cannot remove quick veto for hotwater") + + @property + def component(self) -> Component: + """Return the DHW component.""" + return self.coordinator.data.hotwater + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return HotWater.MIN_TARGET_TEMP + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return HotWater.MAX_TARGET_TEMP + + def _ha_mode(self): + return DHWClimate._HA_MODE_TO_MULTIMATIC + + def _multimatic_mode(self): + return DHWClimate._MULTIMATIC_TO_HA + + def _ha_preset(self): + return DHWClimate._HA_PRESET_TO_MULTIMATIC + + @property + def hvac_action(self) -> HVACAction | None: + """Return the current running hvac operation if supported.""" + should_heat = ( + self.current_temperature is None + or self.current_temperature < self.target_temperature + ) + return ( + HVACAction.HEATING + if should_heat and self.hvac_mode != HVACMode.OFF + else HVACAction.IDLE + ) + + @property + def hvac_mode(self) -> HVACMode | None: + """Return hvac operation ie. heat, cool mode.""" + return DHWClimate._MULTIMATIC_TO_HA[self.active_mode.current][0] + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new target preset mode.""" + mode = DHWClimate._HA_PRESET_TO_MULTIMATIC[preset_mode] + _LOGGER.info("Will set %s operation mode to hot water", mode) + await self.coordinator.api.set_hot_water_operating_mode(self, mode) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + await self.coordinator.api.set_hot_water_target_temperature( + self, kwargs.get(ATTR_TEMPERATURE) + ) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + mode = DHWClimate._HA_MODE_TO_MULTIMATIC[hvac_mode] + await self.coordinator.api.set_hot_water_operating_mode(self, mode) diff --git a/custom_components/multimatic/coordinator.py b/custom_components/multimatic/coordinator.py index 7c71b31..9c321ce 100644 --- a/custom_components/multimatic/coordinator.py +++ b/custom_components/multimatic/coordinator.py @@ -274,8 +274,13 @@ async def set_hot_water_operating_mode(self, entity, mode): hotwater = entity.component touch_system = await self._remove_quick_mode_or_holiday(entity) - await self._manager.set_hot_water_operating_mode(hotwater.id, mode) - hotwater.operating_mode = mode + if isinstance(mode, QuickMode): + await self._hard_set_quick_mode(mode) + self._quick_mode = mode + touch_system = True + else: + await self._manager.set_hot_water_operating_mode(hotwater.id, mode) + hotwater.operating_mode = mode await self._refresh(touch_system, entity) diff --git a/custom_components/multimatic/manifest.json b/custom_components/multimatic/manifest.json index 1470ae1..a1e3f47 100644 --- a/custom_components/multimatic/manifest.json +++ b/custom_components/multimatic/manifest.json @@ -12,7 +12,7 @@ "homekit": {}, "dependencies": [], "codeowners": ["@thomasgermain"], - "version": "1.15.1", + "version": "1.16.0", "iot_class": "cloud_polling", "integration_type": "hub" } diff --git a/hacs.json b/hacs.json index 76f0897..4a150a0 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "multimatic", "render_readme": true, - "homeassistant": "2023.9.0" + "homeassistant": "2023.10.0" }