From 694858e7220fe3a6d6167c92efab718eed54ef1a Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 26 Dec 2024 21:39:53 +0800 Subject: [PATCH] feat: add bath-heater as climate entity --- custom_components/xiaomi_home/climate.py | 238 ++++++++++++++++++ .../xiaomi_home/miot/specs/specv2entity.py | 24 ++ 2 files changed, 262 insertions(+) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index bd4cfe36..44b8ebf0 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -88,6 +88,9 @@ async def async_setup_entry( for data in miot_device.entity_list.get('heater', []): new_entities.append( Heater(miot_device=miot_device, entity_data=data)) + for data in miot_device.entity_list.get('bath-heater', []): + new_entities.append( + BathHeater(miot_device=miot_device, entity_data=data)) if new_entities: async_add_entities(new_entities) @@ -617,3 +620,238 @@ def preset_mode(self) -> Optional[str]: map_=self._heat_level_map, key=self.get_prop_value(prop=self._prop_heat_level)) if self._prop_heat_level else None) + +class BathHeater(MIoTServiceEntity, ClimateEntity): + """Heater entities for Xiaomi Home.""" + # service: ptc-bath-heater + _prop_target_temp: Optional[MIoTSpecProperty] + _prop_heat_level: Optional[MIoTSpecProperty] + _prop_mode: Optional[MIoTSpecProperty] + _prop_env_temp: Optional[MIoTSpecProperty] + # service: fan-control + _prop_fan_on: Optional[MIoTSpecProperty] + _prop_fan_level: Optional[MIoTSpecProperty] + _prop_horizontal_swing: Optional[MIoTSpecProperty] + _prop_vertical_swing: Optional[MIoTSpecProperty] + + _heat_level_map: Optional[dict[int, str]] + _hvac_mode_map: Optional[dict[int, HVACMode]] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + """Initialize the Bath Heater.""" + super().__init__(miot_device=miot_device, entity_data=entity_data) + self._attr_icon = 'mdi:air-conditioner' + self._attr_supported_features = ClimateEntityFeature(0) + self._attr_preset_modes = [] + self._attr_hvac_modes = [] + self._attr_swing_modes = [] + + self._prop_mode = None + self._prop_target_temp = None + self._prop_heat_level = None + self._prop_env_temp = None + self._prop_fan_on = None + self._prop_fan_level = None + self._prop_horizontal_swing = None + self._prop_vertical_swing = None + self._heat_level_map = None + self._hvac_mode_map = None + + # properties + for prop in entity_data.props: + if prop.name == 'target-temperature': + if not isinstance(prop.value_range, dict): + _LOGGER.error( + 'invalid target-temperature value_range format, %s', + self.entity_id) + continue + self._attr_min_temp = prop.value_range['min'] + self._attr_max_temp = prop.value_range['max'] + self._attr_target_temperature_step = prop.value_range['step'] + self._attr_temperature_unit = prop.external_unit + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_TEMPERATURE) + self._prop_target_temp = prop + elif prop.name == 'heat-level': + if ( + not isinstance(prop.value_list, list) + or not prop.value_list + ): + _LOGGER.error( + 'invalid heat-level value_list, %s', self.entity_id) + continue + self._heat_level_map = { + item['value']: item['description'] + for item in prop.value_list} + self._attr_preset_modes = list(self._heat_level_map.values()) + self._attr_supported_features |= ( + ClimateEntityFeature.PRESET_MODE) + self._prop_heat_level = prop + elif prop.name == 'temperature': + self._prop_env_temp = prop + elif prop.name == 'mode': + if ( + not isinstance(prop.value_list, list) + or not prop.value_list + ): + _LOGGER.error( + 'invalid mode value_list, %s', self.entity_id) + continue + self._hvac_mode_map = {} + for item in prop.value_list: + if item['name'].lower() in {'off', 'idle'}: + self._hvac_mode_map[item['value']] = HVACMode.OFF + elif item['name'].lower() in {'auto'}: + self._hvac_mode_map[item['value']] = HVACMode.AUTO + elif item['name'].lower() in {'heat', 'quick heat'}: + self._hvac_mode_map[item['value']] = HVACMode.HEAT + elif item['name'].lower() in {'dry'}: + self._hvac_mode_map[item['value']] = HVACMode.DRY + elif item['name'].lower() in {'fan', 'ventilate'}: + self._hvac_mode_map[item['value']] = HVACMode.FAN_ONLY + self._attr_hvac_modes = list(self._hvac_mode_map.values()) + self._prop_mode = prop + elif prop.name == 'on': + if prop.service.name == 'fan-control': + self._attr_swing_modes.append(SWING_ON) + self._prop_fan_on = prop + elif prop.name == 'fan-level': + if ( + not isinstance(prop.value_list, list) + or not prop.value_list + ): + _LOGGER.error( + 'invalid fan-level value_list, %s', self.entity_id) + continue + self._fan_mode_map = { + item['value']: item['description'] + for item in prop.value_list} + self._attr_fan_modes = list(self._fan_mode_map.values()) + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE + self._prop_fan_level = prop + elif prop.name == 'horizontal-swing': + self._attr_swing_modes.append(SWING_HORIZONTAL) + self._prop_horizontal_swing = prop + elif prop.name == 'vertical-swing': + self._attr_swing_modes.append(SWING_VERTICAL) + # hvac modes + if HVACMode.OFF not in self._attr_hvac_modes: + self._attr_hvac_modes.append(HVACMode.OFF) + # swing modes + if ( + SWING_HORIZONTAL in self._attr_swing_modes + and SWING_VERTICAL in self._attr_swing_modes + ): + self._attr_swing_modes.append(SWING_BOTH) + if self._attr_swing_modes: + self._attr_swing_modes.insert(0, SWING_OFF) + self._attr_supported_features |= ClimateEntityFeature.SWING_MODE + + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set target hvac mode.""" + # set mode + mode_value = self.get_map_value( + map_=self._hvac_mode_map, description=hvac_mode) + if ( + mode_value is None or + not await self.set_property_async( + prop=self._prop_mode, value=mode_value) + ): + raise RuntimeError( + f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}') + + async def async_set_temperature(self, **kwargs): + """Set target temperature.""" + if ATTR_TEMPERATURE in kwargs: + temp = kwargs[ATTR_TEMPERATURE] + if temp > self.max_temp: + temp = self.max_temp + elif temp < self.min_temp: + temp = self.min_temp + + await self.set_property_async( + prop=self._prop_target_temp, value=temp) + + async def async_set_swing_mode(self, swing_mode): + """Set target swing operation.""" + if swing_mode == SWING_BOTH: + if await self.set_property_async( + prop=self._prop_horizontal_swing, value=True, update=False): + self.set_prop_value(self._prop_horizontal_swing, value=True) + if await self.set_property_async( + prop=self._prop_vertical_swing, value=True, update=False): + self.set_prop_value(self._prop_vertical_swing, value=True) + elif swing_mode == SWING_HORIZONTAL: + if await self.set_property_async( + prop=self._prop_horizontal_swing, value=True, update=False): + self.set_prop_value(self._prop_horizontal_swing, value=True) + elif swing_mode == SWING_VERTICAL: + if await self.set_property_async( + prop=self._prop_vertical_swing, value=True, update=False): + self.set_prop_value(self._prop_vertical_swing, value=True) + elif swing_mode == SWING_ON: + if await self.set_property_async( + prop=self._prop_fan_on, value=True, update=False): + self.set_prop_value(self._prop_fan_on, value=True) + elif swing_mode == SWING_OFF: + if self._prop_fan_on and await self.set_property_async( + prop=self._prop_fan_on, value=False, update=False): + self.set_prop_value(self._prop_fan_on, value=False) + if self._prop_horizontal_swing and await self.set_property_async( + prop=self._prop_horizontal_swing, value=False, + update=False): + self.set_prop_value(self._prop_horizontal_swing, value=False) + if self._prop_vertical_swing and await self.set_property_async( + prop=self._prop_vertical_swing, value=False, update=False): + self.set_prop_value(self._prop_vertical_swing, value=False) + else: + raise RuntimeError( + f'unknown swing_mode, {swing_mode}, {self.entity_id}') + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set target fan mode.""" + mode_value = self.get_map_value( + map_=self._fan_mode_map, description=fan_mode) + if mode_value is None or not await self.set_property_async( + prop=self._prop_fan_level, value=mode_value): + raise RuntimeError( + f'set climate prop.fan_mode failed, {fan_mode}, ' + f'{self.entity_id}') + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" + await self.set_property_async( + self._prop_heat_level, + value=self.get_map_value( + map_=self._heat_level_map, description=preset_mode)) + + @property + def target_temperature(self) -> Optional[float]: + """Return the target temperature.""" + return self.get_prop_value( + prop=self._prop_target_temp) if self._prop_target_temp else None + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self.get_prop_value( + prop=self._prop_env_temp) if self._prop_env_temp else None + + @property + def hvac_mode(self) -> Optional[HVACMode]: + """Return the hvac mode. e.g., heat, idle mode.""" + return self.get_map_description( + map_=self._hvac_mode_map, + key=self.get_prop_value(prop=self._prop_mode)) + + @property + def preset_mode(self) -> Optional[str]: + return ( + self.get_map_description( + map_=self._heat_level_map, + key=self.get_prop_value(prop=self._prop_heat_level)) + if self._prop_heat_level else None) diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index d66ab1a0..8b1c5b04 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -234,6 +234,30 @@ }, }, 'entity': 'heater' + }, + 'bath-heater': { + 'required': { + 'ptc-bath-heater': { + 'required': {}, + 'optional': { + 'properties': { + 'target-temperature', 'heat-level', + 'temperature', 'mode' + } + }, + } + }, + 'optional': { + 'fan-control': { + 'required': {}, + 'optional': { + 'properties': { + 'on', 'fan-level', 'horizontal-swing', 'vertical-swing' + } + }, + } + }, + 'entity': 'bath-heater', } }