From 80931aaa42f6839b5d975bdc026d447899bdf0ae Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 26 Dec 2024 19:42:16 +0800 Subject: [PATCH 01/11] feat: add thermostat as climate entity --- custom_components/xiaomi_home/miot/specs/specv2entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 4a578671..d66ab1a0 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -211,6 +211,7 @@ 'entity': 'air-conditioner' }, 'air-condition-outlet': 'air-conditioner', + 'thermostat': 'air-conditioner', 'heater': { 'required': { 'heater': { From 694858e7220fe3a6d6167c92efab718eed54ef1a Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 26 Dec 2024 21:39:53 +0800 Subject: [PATCH 02/11] 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', } } From ff984fae339888ccccbe980eeab58e98c582399d Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 9 Jan 2025 14:10:05 +0800 Subject: [PATCH 03/11] refactor: climate entity --- custom_components/xiaomi_home/climate.py | 1223 +++++++++-------- .../xiaomi_home/miot/specs/specv2entity.py | 24 +- 2 files changed, 639 insertions(+), 608 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 44b8ebf0..367465bb 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -53,7 +53,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.climate import ( - SWING_ON, + FAN_ON, + FAN_OFF, SWING_OFF, SWING_BOTH, SWING_VERTICAL, @@ -61,7 +62,7 @@ ATTR_TEMPERATURE, HVACMode, ClimateEntity, - ClimateEntityFeature + ClimateEntityFeature, ) from .miot.const import DOMAIN @@ -72,357 +73,576 @@ async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a config entry.""" device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ - config_entry.entry_id] + config_entry.entry_id + ] new_entities = [] for miot_device in device_list: for data in miot_device.entity_list.get('air-conditioner', []): new_entities.append( - AirConditioner(miot_device=miot_device, entity_data=data)) + AirConditioner(miot_device=miot_device, entity_data=data) + ) for data in miot_device.entity_list.get('heater', []): new_entities.append( - Heater(miot_device=miot_device, entity_data=data)) + 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)) + PtcBathHeater(miot_device=miot_device, entity_data=data) + ) + for data in miot_device.entity_list.get('thermostat', []): + new_entities.append( + Thermostat(miot_device=miot_device, entity_data=data) + ) if new_entities: async_add_entities(new_entities) -class AirConditioner(MIoTServiceEntity, ClimateEntity): - """Air conditioner entities for Xiaomi Home.""" - # service: air-conditioner - _prop_on: Optional[MIoTSpecProperty] - _prop_mode: Optional[MIoTSpecProperty] - _prop_target_temp: Optional[MIoTSpecProperty] - _prop_target_humi: 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] - # service: environment - _prop_env_temp: Optional[MIoTSpecProperty] - _prop_env_humi: Optional[MIoTSpecProperty] - # service: air-condition-outlet-matching - _prop_ac_state: Optional[MIoTSpecProperty] - _value_ac_state: Optional[dict[str, int]] +class FeatureOnOff(MIoTServiceEntity, ClimateEntity): + """TURN_ON and TURN_OFF feature of the climate entity.""" - _hvac_mode_map: Optional[dict[int, HVACMode]] - _fan_mode_map: Optional[dict[int, str]] + _prop_on: Optional[MIoTSpecProperty] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: - """Initialize the Air conditioner.""" - super().__init__(miot_device=miot_device, entity_data=entity_data) - self._attr_icon = 'mdi:air-conditioner' - self._attr_supported_features = ClimateEntityFeature(0) - self._attr_swing_mode = None - self._attr_swing_modes = [] - self._prop_on = None - self._prop_mode = None - self._prop_target_temp = None - self._prop_target_humi = None - self._prop_fan_on = None - self._prop_fan_level = None - self._prop_horizontal_swing = None - self._prop_vertical_swing = None - self._prop_env_temp = None - self._prop_env_humi = None - self._prop_ac_state = None - self._value_ac_state = None - self._hvac_mode_map = None - self._fan_mode_map = None + super().__init__(miot_device=miot_device, entity_data=entity_data) # properties for prop in entity_data.props: if prop.name == 'on': - if prop.service.name == 'air-conditioner': + if ( + prop.service.name == 'air-conditioner' + or prop.service.name == 'heater' + ): self._attr_supported_features |= ( - ClimateEntityFeature.TURN_ON) + ClimateEntityFeature.TURN_ON + ) self._attr_supported_features |= ( - ClimateEntityFeature.TURN_OFF) + ClimateEntityFeature.TURN_OFF + ) self._prop_on = prop - elif prop.service.name == 'fan-control': - self._attr_swing_modes.append(SWING_ON) - self._prop_fan_on = prop - else: - _LOGGER.error( - 'unknown on property, %s', self.entity_id) - 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 {'cool'}: - self._hvac_mode_map[item['value']] = HVACMode.COOL - elif item['name'].lower() in {'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'}: - 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 == 'target-temperature': + + async def async_turn_on(self) -> None: + """Turn on.""" + await self.set_property_async(prop=self._prop_on, value=True) + + async def async_turn_off(self) -> None: + """Turn off.""" + await self.set_property_async(prop=self._prop_on, value=False) + + +class FeatureTargetTemperature(MIoTServiceEntity, ClimateEntity): + """TARGET_TEMPERATURE feature of the climate entity.""" + + _prop_target_temp: Optional[MIoTSpecProperty] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + self._prop_target_temp = None + + super().__init__(miot_device=miot_device, entity_data=entity_data) + # 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) + 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) + ClimateEntityFeature.TARGET_TEMPERATURE + ) self._prop_target_temp = prop - elif prop.name == 'target-humidity': - if not isinstance(prop.value_range, dict): + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs: + temp = kwargs[ATTR_TEMPERATURE] + if temp > self._attr_max_temp: + temp = self._attr_max_temp + elif temp < self._attr_min_temp: + temp = self._attr_min_temp + + await self.set_property_async( + prop=self._prop_target_temp, value=temp + ) + + @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 + ) + + +class FeaturePresetMode(MIoTServiceEntity, ClimateEntity): + """PRESET_MODE feature of the climate entity.""" + + _prop_mode: Optional[MIoTSpecProperty] + _mode_map: Optional[dict[int, str]] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + self._prop_mode = None + self._mode_map = None + + super().__init__(miot_device=miot_device, entity_data=entity_data) + # properties + for prop in entity_data.props: + if prop.name == 'heat-level' and prop.service.name == 'heater': + if not isinstance(prop.value_list, list) or not prop.value_list: _LOGGER.error( - 'invalid target-humidity value_range format, %s', - self.entity_id) + 'invalid heater heat-level value_list, %s', + self.entity_id, + ) continue - self._attr_min_humidity = prop.value_range['min'] - self._attr_max_humidity = prop.value_range['max'] + self._mode_map = { + item['value']: item['description'] + for item in prop.value_list + } + self._attr_preset_modes = list(self._mode_map.values()) self._attr_supported_features |= ( - ClimateEntityFeature.TARGET_HUMIDITY) - self._prop_target_humi = prop - elif prop.name == 'fan-level': - if ( - not isinstance(prop.value_list, list) - or not prop.value_list - ): + ClimateEntityFeature.PRESET_MODE + ) + self._prop_mode = prop + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" + await self.set_property_async( + self._prop_mode, + value=self.get_map_value( + map_=self._mode_map, description=preset_mode + ), + ) + + @property + def preset_mode(self) -> Optional[str]: + return ( + self.get_map_description( + map_=self._mode_map, + key=self.get_prop_value(prop=self._prop_mode), + ) + if self._prop_mode + else None + ) + + +class FeatureFanMode(MIoTServiceEntity, ClimateEntity): + """FAN_MODE feature of the climate entity.""" + + _prop_fan_on: Optional[MIoTSpecProperty] + _prop_fan_level: Optional[MIoTSpecProperty] + _fan_mode_map: Optional[dict[int, str]] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + self._prop_fan_on = None + self._prop_fan_level = None + self._fan_mode_map = None + + super().__init__(miot_device=miot_device, entity_data=entity_data) + # properties + for prop in entity_data.props: + if 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) + 'invalid fan-level value_list, %s', self.entity_id + ) continue self._fan_mode_map = { item['value']: item['description'] - for item in prop.value_list} + 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) + elif prop.name == 'on' and prop.service.name == 'fan-control': + self._prop_fan_on = prop + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE + + if self._prop_fan_on: + if self._attr_fan_modes is None: + self._attr_fan_modes = [FAN_ON, FAN_OFF] + else: + self._attr_fan_modes.append(FAN_OFF) + + async def async_set_fan_mode(self, fan_mode): + """Set the target fan mode.""" + if fan_mode == FAN_OFF: + await self.set_property_async(prop=self._prop_fan_on, value=False) + return + if fan_mode == FAN_ON: + await self.set_property_async(prop=self._prop_fan_on, value=True) + return + 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}' + ) + + @property + def fan_mode(self) -> Optional[str]: + """The current fan mode.""" + if self._prop_fan_level is None and self._prop_fan_on is None: + return None + if self._prop_fan_level is None and self._prop_fan_on: + return ( + FAN_ON + if self.get_prop_value(prop=self._prop_fan_on) + else FAN_OFF + ) + + return self._fan_mode_map[ + self.get_prop_value(prop=self._prop_fan_level) + ] + + +class FeatureSwingMode(MIoTServiceEntity, ClimateEntity): + """SWING_MODE feature of the climate entity.""" + + _prop_horizontal_swing: Optional[MIoTSpecProperty] + _prop_vertical_swing: Optional[MIoTSpecProperty] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + self._prop_horizontal_swing = None + self._prop_vertical_swing = None + + super().__init__(miot_device=miot_device, entity_data=entity_data) + # properties + swing_modes = [] + for prop in entity_data.props: + if prop.name == 'horizontal-swing': + swing_modes.append(SWING_HORIZONTAL) self._prop_horizontal_swing = prop elif prop.name == 'vertical-swing': - self._attr_swing_modes.append(SWING_VERTICAL) + swing_modes.append(SWING_VERTICAL) self._prop_vertical_swing = prop - elif prop.name == 'temperature': - self._prop_env_temp = prop - elif prop.name == 'relative-humidity': - self._prop_env_humi = prop + # swing modes + if SWING_HORIZONTAL in swing_modes and SWING_VERTICAL in swing_modes: + swing_modes.append(SWING_BOTH) + if swing_modes: + swing_modes.insert(0, SWING_OFF) + self._attr_supported_features |= ClimateEntityFeature.SWING_MODE + self._attr_swing_modes = swing_modes - elif prop.name == 'ac-state': - self._prop_ac_state = prop - self._value_ac_state = {} - self.sub_prop_changed( - prop=prop, handler=self.__ac_state_changed) + async def async_set_swing_mode(self, swing_mode): + """Set the target swing operation.""" + if swing_mode == SWING_BOTH: + await self.set_property_async( + prop=self._prop_horizontal_swing, value=True + ) + await self.set_property_async( + prop=self._prop_vertical_swing, value=True + ) + elif swing_mode == SWING_HORIZONTAL: + await self.set_property_async( + prop=self._prop_horizontal_swing, value=True + ) + elif swing_mode == SWING_VERTICAL: + await self.set_property_async( + prop=self._prop_vertical_swing, value=True + ) + elif swing_mode == SWING_OFF: + if self._prop_horizontal_swing: + await self.set_property_async( + prop=self._prop_horizontal_swing, value=False + ) + if self._prop_vertical_swing: + await self.set_property_async( + prop=self._prop_vertical_swing, value=False + ) + else: + raise RuntimeError( + f'unknown swing_mode, {swing_mode}, {self.entity_id}' + ) - # hvac modes - if HVACMode.OFF not in self._attr_hvac_modes: - self._attr_hvac_modes.append(HVACMode.OFF) - # swing modes + @property + def swing_mode(self) -> Optional[str]: + """The current swing mode of the fan.""" if ( - SWING_HORIZONTAL in self._attr_swing_modes - and SWING_VERTICAL in self._attr_swing_modes + self._prop_horizontal_swing is None + and self._prop_vertical_swing is None ): - 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 + return None + horizontal: bool = ( + self.get_prop_value(prop=self._prop_horizontal_swing) + if self._prop_horizontal_swing + else False + ) + vertical: bool = ( + self.get_prop_value(prop=self._prop_vertical_swing) + if self._prop_vertical_swing + else False + ) + if horizontal and vertical: + return SWING_BOTH + elif horizontal: + return SWING_HORIZONTAL + elif vertical: + return SWING_VERTICAL + else: + return SWING_OFF - async def async_turn_on(self) -> None: - """Turn the entity on.""" - await self.set_property_async(prop=self._prop_on, value=True) - async def async_turn_off(self) -> None: - """Turn the entity off.""" - await self.set_property_async(prop=self._prop_on, value=False) +class FeatureTemperature(MIoTServiceEntity, ClimateEntity): + """Temperature of the climate entity.""" + + _prop_env_temperature: Optional[MIoTSpecProperty] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + self._prop_env_temperature = None + + super().__init__(miot_device=miot_device, entity_data=entity_data) + # properties + for prop in entity_data.props: + if prop.name == 'temperature': + self._prop_env_temperature = prop + + @property + def current_temperature(self) -> Optional[float]: + """The current environment temperature.""" + return ( + self.get_prop_value(prop=self._prop_env_temperature) + if self._prop_env_temperature + else None + ) + + +class FeatureHumidity(MIoTServiceEntity, ClimateEntity): + """Humidity of the climate entity.""" + + _prop_env_humidity: Optional[MIoTSpecProperty] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + self._prop_env_humidity = None + + super().__init__(miot_device=miot_device, entity_data=entity_data) + # properties + for prop in entity_data.props: + if prop.name == 'relative-humidity': + self._prop_env_humidity = prop + + @property + def current_humidity(self) -> Optional[float]: + """The current environment humidity.""" + return ( + self.get_prop_value(prop=self._prop_env_humidity) + if self._prop_env_humidity + else None + ) + + +class FeatureTargetHumidity(MIoTServiceEntity, ClimateEntity): + """TARGET_HUMIDITY feature of the climate entity.""" + + _prop_target_humidity: Optional[MIoTSpecProperty] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + self._prop_target_humidity = None + + super().__init__(miot_device=miot_device, entity_data=entity_data) + # properties + for prop in entity_data.props: + if prop.name == 'target-humidity': + if not isinstance(prop.value_range, dict): + _LOGGER.error( + 'invalid target-humidity value_range format, %s', + self.entity_id, + ) + continue + self._attr_min_humidity = prop.value_range['min'] + self._attr_max_humidity = prop.value_range['max'] + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_HUMIDITY + ) + self._prop_target_humidity = prop + + async def async_set_humidity(self, humidity): + """Set the target humidity.""" + if humidity > self._attr_max_humidity: + humidity = self._attr_max_humidity + elif humidity < self._attr_min_humidity: + humidity = self._attr_min_humidity + await self.set_property_async( + prop=self._prop_target_humidity, value=humidity + ) + + @property + def target_humidity(self) -> Optional[int]: + """The current target humidity.""" + return ( + self.get_prop_value(prop=self._prop_target_humidity) + if self._prop_target_humidity + else None + ) + + +class Heater( + FeatureOnOff, + FeatureTargetTemperature, + FeatureTemperature, + FeatureHumidity, + FeaturePresetMode, +): + """Heater""" + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + """Initialize the Heater.""" + super().__init__(miot_device=miot_device, entity_data=entity_data) + + self._attr_icon = 'mdi:radiator' + # hvac modes + self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set new target hvac mode.""" - # set air-conditioner off + """Set the target hvac mode.""" + await self.set_property_async( + prop=self._prop_on, + value=False if hvac_mode == HVACMode.OFF else True, + ) + + @property + def hvac_mode(self) -> Optional[HVACMode]: + """The current hvac mode.""" + return ( + HVACMode.HEAT + if self.get_prop_value(prop=self._prop_on) + else HVACMode.OFF + ) + + +class AirConditioner( + FeatureOnOff, + FeatureTargetTemperature, + FeatureTargetHumidity, + FeatureTemperature, + FeatureHumidity, + FeatureFanMode, + FeatureSwingMode, +): + """Air conditioner""" + + _prop_mode: Optional[MIoTSpecProperty] + _hvac_mode_map: Optional[dict[int, HVACMode]] + _prop_ac_state: Optional[MIoTSpecProperty] + _value_ac_state: Optional[dict[str, int]] + + def __init__( + self, miot_device: MIoTDevice, entity_data: MIoTEntityData + ) -> None: + """Initialize the Heater.""" + self._prop_mode = None + self._hvac_mode_map = None + self._prop_ac_state = None + self._value_ac_state = None + + super().__init__(miot_device=miot_device, entity_data=entity_data) + self._attr_icon = 'mdi:air-conditioner' + # hvac modes + for prop in entity_data.props: + if 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 {'cool'}: + self._hvac_mode_map[item['value']] = HVACMode.COOL + elif item['name'].lower() in {'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'}: + 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 == 'ac-state': + self._prop_ac_state = prop + self._value_ac_state = {} + self.sub_prop_changed( + prop=prop, handler=self.__ac_state_changed + ) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set the target hvac mode.""" + # set the device off if hvac_mode == HVACMode.OFF: if not await self.set_property_async( - prop=self._prop_on, value=False): + prop=self._prop_on, value=False + ): raise RuntimeError( f'set climate prop.on failed, {hvac_mode}, ' - f'{self.entity_id}') + f'{self.entity_id}' + ) return - # set air-conditioner on + # set the device on elif self.get_prop_value(prop=self._prop_on) is False: await self.set_property_async(prop=self._prop_on, value=True) # set mode + if self._prop_mode is None: + return 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 new 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_humidity(self, humidity): - """Set new target humidity.""" - if humidity > self.max_humidity: - humidity = self.max_humidity - elif humidity < self.min_humidity: - humidity = self.min_humidity - await self.set_property_async( - prop=self._prop_target_humi, value=humidity) - - async def async_set_swing_mode(self, swing_mode): - """Set new 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 new target fan mode.""" - mode_value = self.get_map_value( - map_=self._fan_mode_map, description=fan_mode) + map_=self._hvac_mode_map, description=hvac_mode + ) if mode_value is None or not await self.set_property_async( - prop=self._prop_fan_level, value=mode_value): + prop=self._prop_mode, value=mode_value + ): raise RuntimeError( - f'set climate prop.fan_mode failed, {fan_mode}, ' - f'{self.entity_id}') - - @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 target_humidity(self) -> Optional[int]: - """Return the target humidity.""" - return self.get_prop_value( - prop=self._prop_target_humi) if self._prop_target_humi 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 current_humidity(self) -> Optional[int]: - """Return the current humidity.""" - return self.get_prop_value( - prop=self._prop_env_humi) if self._prop_env_humi else None + f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}' + ) @property def hvac_mode(self) -> Optional[HVACMode]: - """Return the hvac mode. e.g., heat, cool mode.""" + """The current hvac mode.""" if self.get_prop_value(prop=self._prop_on) is False: return HVACMode.OFF return self.get_map_description( map_=self._hvac_mode_map, - key=self.get_prop_value(prop=self._prop_mode)) - - @property - def fan_mode(self) -> Optional[str]: - """Return the fan mode. - - Requires ClimateEntityFeature.FAN_MODE. - """ - return self.get_map_description( - map_=self._fan_mode_map, - key=self.get_prop_value(prop=self._prop_fan_level)) - - @property - def swing_mode(self) -> Optional[str]: - """Return the swing mode. - - Requires ClimateEntityFeature.SWING_MODE. - """ - horizontal: bool = ( - self.get_prop_value(prop=self._prop_horizontal_swing) - if self._prop_horizontal_swing else None) - vertical: bool = ( - self.get_prop_value(prop=self._prop_vertical_swing) - if self._prop_vertical_swing else None) - if horizontal and vertical: - return SWING_BOTH - if horizontal: - return SWING_HORIZONTAL - if vertical: - return SWING_VERTICAL - if self._prop_fan_on: - if self.get_prop_value(prop=self._prop_fan_on): - return SWING_ON - else: - return SWING_OFF - return None + key=self.get_prop_value(prop=self._prop_mode) + ) if self._prop_mode else None def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: del prop if not isinstance(value, str): - _LOGGER.error( - 'ac_status value format error, %s', value) + _LOGGER.error('ac_status value format error, %s', value) return v_ac_state = {} v_split = value.split('_') @@ -436,8 +656,7 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: _LOGGER.error('ac_status value error, %s', item) # P: status. 0: on, 1: off if 'P' in v_ac_state and self._prop_on: - self.set_prop_value(prop=self._prop_on, - value=v_ac_state['P'] == 0) + self.set_prop_value(prop=self._prop_on, value=v_ac_state['P'] == 0) # M: model. 0: cool, 1: heat, 2: auto, 3: fan, 4: dry if 'M' in v_ac_state and self._prop_mode: mode: Optional[HVACMode] = { @@ -445,20 +664,25 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: 1: HVACMode.HEAT, 2: HVACMode.AUTO, 3: HVACMode.FAN_ONLY, - 4: HVACMode.DRY + 4: HVACMode.DRY, }.get(v_ac_state['M'], None) if mode: self.set_prop_value( - prop=self._prop_mode, value=self.get_map_value( - map_=self._hvac_mode_map, description=mode)) + prop=self._prop_mode, + value=self.get_map_value( + map_=self._hvac_mode_map, description=mode + ), + ) # T: target temperature if 'T' in v_ac_state and self._prop_target_temp: - self.set_prop_value(prop=self._prop_target_temp, - value=v_ac_state['T']) + self.set_prop_value( + prop=self._prop_target_temp, value=v_ac_state['T'] + ) # S: fan level. 0: auto, 1: low, 2: media, 3: high if 'S' in v_ac_state and self._prop_fan_level: - self.set_prop_value(prop=self._prop_fan_level, - value=v_ac_state['S']) + self.set_prop_value( + prop=self._prop_fan_level, value=v_ac_state['S'] + ) # D: swing mode. 0: on, 1: off if 'D' in v_ac_state and len(self._attr_swing_modes) == 2: if ( @@ -466,238 +690,116 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: and self._prop_horizontal_swing ): self.set_prop_value( - prop=self._prop_horizontal_swing, - value=v_ac_state['D'] == 0) + prop=self._prop_horizontal_swing, value=v_ac_state['D'] == 0 + ) elif ( SWING_VERTICAL in self._attr_swing_modes and self._prop_vertical_swing ): self.set_prop_value( - prop=self._prop_vertical_swing, - value=v_ac_state['D'] == 0) + prop=self._prop_vertical_swing, value=v_ac_state['D'] == 0 + ) self._value_ac_state.update(v_ac_state) - _LOGGER.debug( - 'ac_state update, %s', self._value_ac_state) + _LOGGER.debug('ac_state update, %s', self._value_ac_state) -class Heater(MIoTServiceEntity, ClimateEntity): - """Heater entities for Xiaomi Home.""" - # service: heater - _prop_on: Optional[MIoTSpecProperty] - _prop_mode: Optional[MIoTSpecProperty] - _prop_target_temp: Optional[MIoTSpecProperty] - _prop_heat_level: Optional[MIoTSpecProperty] - # service: environment - _prop_env_temp: Optional[MIoTSpecProperty] - _prop_env_humi: Optional[MIoTSpecProperty] +class PtcBathHeater( + FeatureTargetTemperature, + FeatureTemperature, + FeatureFanMode, + FeatureSwingMode, +): + """Ptc bath heater""" - _heat_level_map: Optional[dict[int, str]] + _prop_mode: Optional[MIoTSpecProperty] + _hvac_mode_map: Optional[dict[int, HVACMode]] def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: - """Initialize the 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._prop_on = None + """Initialize the ptc bath heater.""" self._prop_mode = None - self._prop_target_temp = None - self._prop_heat_level = None - self._prop_env_temp = None - self._prop_env_humi = None - self._heat_level_map = None + self._hvac_mode_map = None - # properties + super().__init__(miot_device=miot_device, entity_data=entity_data) + self._attr_icon = 'mdi:hvac' + # hvac modes for prop in entity_data.props: - if prop.name == 'on': - self._attr_supported_features |= ( - ClimateEntityFeature.TURN_ON) - self._attr_supported_features |= ( - ClimateEntityFeature.TURN_OFF) - self._prop_on = prop - elif prop.name == 'target-temperature': - if not isinstance(prop.value_range, dict): - _LOGGER.error( - 'invalid target-temperature value_range format, %s', - self.entity_id) + if 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._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 == 'relative-humidity': - self._prop_env_humi = prop - - # hvac modes - self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] - - async def async_turn_on(self) -> None: - """Turn the entity on.""" - await self.set_property_async(prop=self._prop_on, value=True) - - async def async_turn_off(self) -> None: - """Turn the entity off.""" - await self.set_property_async(prop=self._prop_on, value=False) + 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 {'ventilate'}: + self._hvac_mode_map[item['value']] = HVACMode.COOL + elif item['name'].lower() in {'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'}: + self._hvac_mode_map[item['value']] = HVACMode.FAN_ONLY + self._attr_hvac_modes = list(self._hvac_mode_map.values()) + self._prop_mode = prop async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set new target hvac mode.""" - await self.set_property_async( - prop=self._prop_on, value=False - if hvac_mode == HVACMode.OFF else True) - - async def async_set_temperature(self, **kwargs): - """Set new 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_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 current_humidity(self) -> Optional[int]: - """Return the current humidity.""" - return self.get_prop_value( - prop=self._prop_env_humi) if self._prop_env_humi else None + """Set the target hvac mode.""" + if self._prop_mode is None: + return + 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}' + ) @property def hvac_mode(self) -> Optional[HVACMode]: - """Return the hvac mode.""" - return ( - HVACMode.HEAT if self.get_prop_value(prop=self._prop_on) - else HVACMode.OFF) - - @property - def preset_mode(self) -> Optional[str]: + """The current hvac mode.""" 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) + map_=self._hvac_mode_map, + key=self.get_prop_value(prop=self._prop_mode), + ) + if self._prop_mode + else None + ) + + +class Thermostat( + FeatureOnOff, + FeatureTargetTemperature, + FeatureTemperature, + FeatureHumidity, + FeatureFanMode, +): + """Thermostat""" -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 = [] - + """Initialize the thermostat.""" 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 + super().__init__(miot_device=miot_device, entity_data=entity_data) + self._attr_icon = 'mdi:thermostat' + # hvac modes 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) + if 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: @@ -705,153 +807,60 @@ def __init__( 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'}: + elif item['name'].lower() in {'cool'}: + self._hvac_mode_map[item['value']] = HVACMode.COOL + elif item['name'].lower() in {'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'}: + elif item['name'].lower() in {'fan'}: 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 + if self._attr_hvac_modes is None: + self._attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO] + elif HVACMode.OFF not in self._attr_hvac_modes: + self._attr_hvac_modes.insert(0, HVACMode.OFF) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set target hvac mode.""" + """Set new target hvac mode.""" + # set the device off + if hvac_mode == HVACMode.OFF: + if not await self.set_property_async( + prop=self._prop_on, value=False + ): + raise RuntimeError( + f'set climate prop.on failed, {hvac_mode}, ' + f'{self.entity_id}' + ) + return + # set the device on + elif self.get_prop_value(prop=self._prop_on) is False: + await self.set_property_async(prop=self._prop_on, value=True) # set mode + if self._prop_mode is None: + return 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) + map_=self._hvac_mode_map, description=hvac_mode + ) if mode_value is None or not await self.set_property_async( - prop=self._prop_fan_level, value=mode_value): + prop=self._prop_mode, 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 + f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}' + ) @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]: + """The current hvac mode.""" + if self.get_prop_value(prop=self._prop_on) is False: + return HVACMode.OFF 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) + map_=self._hvac_mode_map, + key=self.get_prop_value(prop=self._prop_mode), + ) + if self._prop_mode + else None + ) diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 39b5b0f0..775375b9 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -219,7 +219,29 @@ 'entity': 'air-conditioner' }, 'air-condition-outlet': 'air-conditioner', - 'thermostat': 'air-conditioner', + 'thermostat': { + 'required': { + 'thermostat': { + 'required': { + 'properties': { + 'on': {'read', 'write'} + } + }, + 'optional': { + 'properties': {'target-temperature', 'mode', 'fan-level'} + }, + } + }, + 'optional': { + 'environment': { + 'required': {}, + 'optional': { + 'properties': {'temperature', 'relative-humidity'} + } + }, + }, + 'entity': 'thermostat' + }, 'heater': { 'required': { 'heater': { From fe3e1b40058a21e3e615e292e92e6107f0b4039e Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 9 Jan 2025 16:02:58 +0800 Subject: [PATCH 04/11] fix: thermostat on/off --- custom_components/xiaomi_home/climate.py | 15 +++++++++++---- .../xiaomi_home/miot/specs/specv2entity.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 367465bb..d2b3ce3c 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -120,8 +120,11 @@ def __init__( for prop in entity_data.props: if prop.name == 'on': if ( + # The "on" property of the "fan-control" service is not + # the on/off feature of the entity. prop.service.name == 'air-conditioner' or prop.service.name == 'heater' + or prop.service.name == 'thermostat' ): self._attr_supported_features |= ( ClimateEntityFeature.TURN_ON @@ -634,10 +637,14 @@ def hvac_mode(self) -> Optional[HVACMode]: """The current hvac mode.""" if self.get_prop_value(prop=self._prop_on) is False: return HVACMode.OFF - return self.get_map_description( - map_=self._hvac_mode_map, - key=self.get_prop_value(prop=self._prop_mode) - ) if self._prop_mode else None + return ( + self.get_map_description( + map_=self._hvac_mode_map, + key=self.get_prop_value(prop=self._prop_mode), + ) + if self._prop_mode + else None + ) def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: del prop diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 775375b9..e0d9dd55 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -228,7 +228,8 @@ } }, 'optional': { - 'properties': {'target-temperature', 'mode', 'fan-level'} + 'properties': {'target-temperature', 'mode', 'fan-level', + 'temperature'} }, } }, From 31ed45faf81f93597c4f93998d243efd46874044 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 9 Jan 2025 17:25:02 +0800 Subject: [PATCH 05/11] fix: get the current fan mode --- custom_components/xiaomi_home/climate.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index d2b3ce3c..f19f8520 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -265,7 +265,7 @@ def __init__( super().__init__(miot_device=miot_device, entity_data=entity_data) # properties for prop in entity_data.props: - if prop.name == 'fan-level': + if prop.name == 'fan-level' and prop.service.name == 'fan-control': if not isinstance(prop.value_list, list) or not prop.value_list: _LOGGER.error( 'invalid fan-level value_list, %s', self.entity_id @@ -319,9 +319,8 @@ def fan_mode(self) -> Optional[str]: else FAN_OFF ) - return self._fan_mode_map[ - self.get_prop_value(prop=self._prop_fan_level) - ] + fan_level = self.get_prop_value(prop=self._prop_fan_level) + return None if fan_level is None else self._fan_mode_map[fan_level] class FeatureSwingMode(MIoTServiceEntity, ClimateEntity): @@ -604,6 +603,11 @@ def __init__( prop=prop, handler=self.__ac_state_changed ) + if self._attr_hvac_modes is None: + self._attr_hvac_modes = [HVACMode.OFF] + elif HVACMode.OFF not in self._attr_hvac_modes: + self._attr_hvac_modes.append(HVACMode.OFF) + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the target hvac mode.""" # set the device off From 3300eeb5955c6c0301498d454d0b35f186d83eb9 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Fri, 10 Jan 2025 12:28:06 +0800 Subject: [PATCH 06/11] perf: get fan level --- custom_components/xiaomi_home/climate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index f19f8520..ffd7ce4e 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -318,9 +318,10 @@ def fan_mode(self) -> Optional[str]: if self.get_prop_value(prop=self._prop_fan_on) else FAN_OFF ) - - fan_level = self.get_prop_value(prop=self._prop_fan_level) - return None if fan_level is None else self._fan_mode_map[fan_level] + return self.get_map_description( + map_=self._fan_mode_map, + key=self.get_prop_value(prop=self._prop_fan_level) + ) class FeatureSwingMode(MIoTServiceEntity, ClimateEntity): From 9b14d1b0861d6ba6648406fc10cf95282a582c70 Mon Sep 17 00:00:00 2001 From: topsworld Date: Tue, 21 Jan 2025 20:08:04 +0800 Subject: [PATCH 07/11] fix: fix climate hvac_mode --- custom_components/xiaomi_home/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 7051c83e..7cbcfac3 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -700,9 +700,9 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: def hvac_mode(self) -> Optional[HVACMode]: """The current hvac mode.""" return ( - self.get_map_key( + self.get_map_value( map_=self._hvac_mode_map, - value=self.get_prop_value(prop=self._prop_mode)) + key=self.get_prop_value(prop=self._prop_mode)) if self._prop_mode else None) From 9562049af9d43f0f5219341f39d455f7fc5d43c3 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Wed, 22 Jan 2025 18:11:55 +0800 Subject: [PATCH 08/11] fix: misuse of getting key or value from dict[int, any] --- custom_components/xiaomi_home/climate.py | 50 ++++++++++++------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 7cbcfac3..9df58adf 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -62,7 +62,7 @@ ATTR_TEMPERATURE, HVACMode, ClimateEntity, - ClimateEntityFeature, + ClimateEntityFeature ) from .miot.const import DOMAIN @@ -75,7 +75,7 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback ) -> None: """Set up a config entry.""" device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ @@ -211,14 +211,14 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode.""" await self.set_property_async( self._prop_mode, - value=self.get_map_value(map_=self._mode_map, key=preset_mode)) + value=self.get_map_key(map_=self._mode_map, value=preset_mode)) @property def preset_mode(self) -> Optional[str]: return ( - self.get_map_key( + self.get_map_value( map_=self._mode_map, - value=self.get_prop_value(prop=self._prop_mode)) + key=self.get_prop_value(prop=self._prop_mode)) if self._prop_mode else None) @@ -265,8 +265,8 @@ async def async_set_fan_mode(self, fan_mode): if fan_mode == FAN_ON: await self.set_property_async(prop=self._prop_fan_on, value=True) return - mode_value = self.get_map_value( - map_=self._fan_mode_map, key=fan_mode) + mode_value = self.get_map_key( + map_=self._fan_mode_map, value=fan_mode) if mode_value is None or not await self.set_property_async( prop=self._prop_fan_level, value=mode_value ): @@ -283,9 +283,9 @@ def fan_mode(self) -> Optional[str]: return ( FAN_ON if self.get_prop_value(prop=self._prop_fan_on) else FAN_OFF) - return self.get_map_key( + return self.get_map_value( map_=self._fan_mode_map, - value=self.get_prop_value(prop=self._prop_fan_level)) + key=self.get_prop_value(prop=self._prop_fan_level)) class FeatureSwingMode(MIoTServiceEntity, ClimateEntity): @@ -457,7 +457,7 @@ class Heater( FeatureTargetTemperature, FeatureTemperature, FeatureHumidity, - FeaturePresetMode, + FeaturePresetMode ): """Heater""" @@ -492,7 +492,7 @@ class AirConditioner( FeatureTemperature, FeatureHumidity, FeatureFanMode, - FeatureSwingMode, + FeatureSwingMode ): """Air conditioner""" _prop_mode: Optional[MIoTSpecProperty] @@ -562,8 +562,8 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # set mode if self._prop_mode is None: return - mode_value = self.get_map_value( - map_=self._hvac_mode_map, key=hvac_mode) + mode_value = self.get_map_key( + map_=self._hvac_mode_map, value=hvac_mode) if mode_value is None or not await self.set_property_async( prop=self._prop_mode, value=mode_value ): @@ -576,9 +576,9 @@ def hvac_mode(self) -> Optional[HVACMode]: if self.get_prop_value(prop=self._prop_on) is False: return HVACMode.OFF return ( - self.get_map_key( + self.get_map_value( map_=self._hvac_mode_map, - value=self.get_prop_value(prop=self._prop_mode)) + key=self.get_prop_value(prop=self._prop_mode)) if self._prop_mode else None) def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: @@ -611,8 +611,8 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: if mode: self.set_prop_value( prop=self._prop_mode, - value=self.get_map_value( - map_=self._hvac_mode_map, key=mode)) + value=self.get_map_key( + map_=self._hvac_mode_map, value=mode)) # T: target temperature if 'T' in v_ac_state and self._prop_target_temp: self.set_prop_value( @@ -645,7 +645,7 @@ class PtcBathHeater( FeatureTargetTemperature, FeatureTemperature, FeatureFanMode, - FeatureSwingMode, + FeatureSwingMode ): """Ptc bath heater""" _prop_mode: Optional[MIoTSpecProperty] @@ -688,8 +688,8 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the target hvac mode.""" if self._prop_mode is None: return - mode_value = self.get_map_value( - map_=self._hvac_mode_map, key=hvac_mode) + mode_value = self.get_map_key( + map_=self._hvac_mode_map, value=hvac_mode) if mode_value is None or not await self.set_property_async( prop=self._prop_mode, value=mode_value ): @@ -711,7 +711,7 @@ class Thermostat( FeatureTargetTemperature, FeatureTemperature, FeatureHumidity, - FeatureFanMode, + FeatureFanMode ): """Thermostat""" _prop_mode: Optional[MIoTSpecProperty] @@ -772,8 +772,8 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # set mode if self._prop_mode is None: return - mode_value = self.get_map_value( - map_=self._hvac_mode_map, key=hvac_mode + mode_value = self.get_map_key( + map_=self._hvac_mode_map, value=hvac_mode ) if mode_value is None or not await self.set_property_async( prop=self._prop_mode, value=mode_value @@ -787,7 +787,7 @@ def hvac_mode(self) -> Optional[HVACMode]: if self.get_prop_value(prop=self._prop_on) is False: return HVACMode.OFF return ( - self.get_map_key( + self.get_map_value( map_=self._hvac_mode_map, - value=self.get_prop_value(prop=self._prop_mode)) + key=self.get_prop_value(prop=self._prop_mode)) if self._prop_mode else None) From 0c0cc8d5281a2091e645f812a8cceb5a18233891 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 23 Jan 2025 09:53:11 +0800 Subject: [PATCH 09/11] style: add comments --- custom_components/xiaomi_home/climate.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 9df58adf..27d64821 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -107,6 +107,7 @@ class FeatureOnOff(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: + """Initialize the feature class.""" self._prop_on = None super().__init__(miot_device=miot_device, entity_data=entity_data) @@ -142,6 +143,7 @@ class FeatureTargetTemperature(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: + """Initialize the feature class.""" self._prop_target_temp = None super().__init__(miot_device=miot_device, entity_data=entity_data) @@ -162,7 +164,7 @@ def __init__( self._prop_target_temp = prop async def async_set_temperature(self, **kwargs): - """Set new target temperature.""" + """Set the target temperature.""" if ATTR_TEMPERATURE in kwargs: temp = kwargs[ATTR_TEMPERATURE] if temp > self._attr_max_temp: @@ -175,7 +177,7 @@ async def async_set_temperature(self, **kwargs): @property def target_temperature(self) -> Optional[float]: - """Return the target temperature.""" + """The current target temperature.""" return ( self.get_prop_value(prop=self._prop_target_temp) if self._prop_target_temp else None) @@ -189,6 +191,7 @@ class FeaturePresetMode(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: + """Initialize the feature class.""" self._prop_mode = None self._mode_map = None @@ -215,6 +218,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: @property def preset_mode(self) -> Optional[str]: + """The current preset mode.""" return ( self.get_map_value( map_=self._mode_map, @@ -231,6 +235,7 @@ class FeatureFanMode(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: + """Initialize the feature class.""" self._prop_fan_on = None self._prop_fan_level = None self._fan_mode_map = None @@ -296,6 +301,7 @@ class FeatureSwingMode(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: + """Initialize the feature class.""" self._prop_horizontal_swing = None self._prop_vertical_swing = None @@ -372,6 +378,7 @@ class FeatureTemperature(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: + """Initialize the feature class.""" self._prop_env_temperature = None super().__init__(miot_device=miot_device, entity_data=entity_data) @@ -395,6 +402,7 @@ class FeatureHumidity(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: + """Initialize the feature class.""" self._prop_env_humidity = None super().__init__(miot_device=miot_device, entity_data=entity_data) @@ -418,6 +426,7 @@ class FeatureTargetHumidity(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: + """Initialize the feature class.""" self._prop_target_humidity = None super().__init__(miot_device=miot_device, entity_data=entity_data) @@ -464,7 +473,7 @@ class Heater( def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: - """Initialize the Heater.""" + """Initialize the heater.""" super().__init__(miot_device=miot_device, entity_data=entity_data) self._attr_icon = 'mdi:radiator' @@ -503,7 +512,7 @@ class AirConditioner( def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: - """Initialize the Heater.""" + """Initialize the air conditioner.""" self._prop_mode = None self._hvac_mode_map = None self._prop_ac_state = None @@ -756,7 +765,7 @@ def __init__( self._attr_hvac_modes.insert(0, HVACMode.OFF) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set new target hvac mode.""" + """Set the target hvac mode.""" # set the device off if hvac_mode == HVACMode.OFF: if not await self.set_property_async( From ad8ca02fa1dcebff13ec9d319949ebc7c4d5812f Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 23 Jan 2025 20:04:52 +0800 Subject: [PATCH 10/11] style: format the file based on google style --- custom_components/xiaomi_home/climate.py | 380 +++++++++-------------- 1 file changed, 148 insertions(+), 232 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 30ab8301..ba7619e1 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -53,17 +53,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.climate import ( - FAN_ON, - FAN_OFF, - SWING_OFF, - SWING_BOTH, - SWING_VERTICAL, - SWING_HORIZONTAL, - ATTR_TEMPERATURE, - HVACMode, - ClimateEntity, - ClimateEntityFeature -) + FAN_ON, FAN_OFF, SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL, + ATTR_TEMPERATURE, HVACMode, ClimateEntity, ClimateEntityFeature) from .miot.const import DOMAIN from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData @@ -72,11 +63,8 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback -) -> None: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback) -> None: """Set up a config entry.""" device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][ config_entry.entry_id] @@ -104,9 +92,8 @@ class FeatureOnOff(MIoTServiceEntity, ClimateEntity): """TURN_ON and TURN_OFF feature of the climate entity.""" _prop_on: Optional[MIoTSpecProperty] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the feature class.""" self._prop_on = None @@ -115,12 +102,11 @@ def __init__( for prop in entity_data.props: if prop.name == 'on': if ( - # The "on" property of the "fan-control" service is not - # the on/off feature of the entity. - prop.service.name == 'air-conditioner' - or prop.service.name == 'heater' - or prop.service.name == 'thermostat' - ): + # The "on" property of the "fan-control" service is not + # the on/off feature of the entity. + prop.service.name == 'air-conditioner' or + prop.service.name == 'heater' or + prop.service.name == 'thermostat'): self._attr_supported_features |= ( ClimateEntityFeature.TURN_ON) self._attr_supported_features |= ( @@ -140,9 +126,8 @@ class FeatureTargetTemperature(MIoTServiceEntity, ClimateEntity): """TARGET_TEMPERATURE feature of the climate entity.""" _prop_target_temp: Optional[MIoTSpecProperty] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the feature class.""" self._prop_target_temp = None @@ -172,15 +157,14 @@ async def async_set_temperature(self, **kwargs): elif temp < self._attr_min_temp: temp = self._attr_min_temp - await self.set_property_async( - prop=self._prop_target_temp, value=temp) + await self.set_property_async(prop=self._prop_target_temp, + value=temp) @property def target_temperature(self) -> Optional[float]: """The current target temperature.""" - return ( - self.get_prop_value(prop=self._prop_target_temp) - if self._prop_target_temp else None) + return (self.get_prop_value( + prop=self._prop_target_temp) if self._prop_target_temp else None) class FeaturePresetMode(MIoTServiceEntity, ClimateEntity): @@ -188,9 +172,8 @@ class FeaturePresetMode(MIoTServiceEntity, ClimateEntity): _prop_mode: Optional[MIoTSpecProperty] _mode_map: Optional[dict[int, str]] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the feature class.""" self._prop_mode = None self._mode_map = None @@ -200,9 +183,8 @@ def __init__( for prop in entity_data.props: if prop.name == 'heat-level' and prop.service.name == 'heater': if not prop.value_list: - _LOGGER.error( - 'invalid heater heat-level value_list, %s', - self.entity_id) + _LOGGER.error('invalid heater heat-level value_list, %s', + self.entity_id) continue self._mode_map = prop.value_list.to_map() self._attr_preset_modes = prop.value_list.descriptions @@ -212,18 +194,17 @@ def __init__( async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode.""" - await self.set_property_async( - self._prop_mode, - value=self.get_map_key(map_=self._mode_map, value=preset_mode)) + await self.set_property_async(self._prop_mode, + value=self.get_map_key( + map_=self._mode_map, + value=preset_mode)) @property def preset_mode(self) -> Optional[str]: """The current preset mode.""" - return ( - self.get_map_value( - map_=self._mode_map, - key=self.get_prop_value(prop=self._prop_mode)) - if self._prop_mode else None) + return (self.get_map_value( + map_=self._mode_map, key=self.get_prop_value( + prop=self._prop_mode)) if self._prop_mode else None) class FeatureFanMode(MIoTServiceEntity, ClimateEntity): @@ -232,9 +213,8 @@ class FeatureFanMode(MIoTServiceEntity, ClimateEntity): _prop_fan_level: Optional[MIoTSpecProperty] _fan_mode_map: Optional[dict[int, str]] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the feature class.""" self._prop_fan_on = None self._prop_fan_level = None @@ -245,8 +225,8 @@ def __init__( for prop in entity_data.props: if prop.name == 'fan-level' and prop.service.name == 'fan-control': if not prop.value_list: - _LOGGER.error( - 'invalid fan-level value_list, %s', self.entity_id) + _LOGGER.error('invalid fan-level value_list, %s', + self.entity_id) continue self._fan_mode_map = prop.value_list.to_map() self._attr_fan_modes = prop.value_list.descriptions @@ -270,14 +250,11 @@ async def async_set_fan_mode(self, fan_mode): if fan_mode == FAN_ON: await self.set_property_async(prop=self._prop_fan_on, value=True) return - mode_value = self.get_map_key( - map_=self._fan_mode_map, value=fan_mode) + mode_value = self.get_map_key(map_=self._fan_mode_map, value=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}') + prop=self._prop_fan_level, value=mode_value): + raise RuntimeError(f'set climate prop.fan_mode failed, {fan_mode}, ' + f'{self.entity_id}') @property def fan_mode(self) -> Optional[str]: @@ -285,9 +262,8 @@ def fan_mode(self) -> Optional[str]: if self._prop_fan_level is None and self._prop_fan_on is None: return None if self._prop_fan_level is None and self._prop_fan_on: - return ( - FAN_ON if self.get_prop_value(prop=self._prop_fan_on) - else FAN_OFF) + return (FAN_ON if self.get_prop_value( + prop=self._prop_fan_on) else FAN_OFF) return self.get_map_value( map_=self._fan_mode_map, key=self.get_prop_value(prop=self._prop_fan_level)) @@ -298,9 +274,8 @@ class FeatureSwingMode(MIoTServiceEntity, ClimateEntity): _prop_horizontal_swing: Optional[MIoTSpecProperty] _prop_vertical_swing: Optional[MIoTSpecProperty] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the feature class.""" self._prop_horizontal_swing = None self._prop_vertical_swing = None @@ -326,23 +301,23 @@ def __init__( async def async_set_swing_mode(self, swing_mode): """Set the target swing operation.""" if swing_mode == SWING_BOTH: - await self.set_property_async( - prop=self._prop_horizontal_swing, value=True) - await self.set_property_async( - prop=self._prop_vertical_swing, value=True) + await self.set_property_async(prop=self._prop_horizontal_swing, + value=True) + await self.set_property_async(prop=self._prop_vertical_swing, + value=True) elif swing_mode == SWING_HORIZONTAL: - await self.set_property_async( - prop=self._prop_horizontal_swing, value=True) + await self.set_property_async(prop=self._prop_horizontal_swing, + value=True) elif swing_mode == SWING_VERTICAL: - await self.set_property_async( - prop=self._prop_vertical_swing, value=True) + await self.set_property_async(prop=self._prop_vertical_swing, + value=True) elif swing_mode == SWING_OFF: if self._prop_horizontal_swing: - await self.set_property_async( - prop=self._prop_horizontal_swing, value=False) + await self.set_property_async(prop=self._prop_horizontal_swing, + value=False) if self._prop_vertical_swing: - await self.set_property_async( - prop=self._prop_vertical_swing, value=False) + await self.set_property_async(prop=self._prop_vertical_swing, + value=False) else: raise RuntimeError( f'unknown swing_mode, {swing_mode}, {self.entity_id}') @@ -350,17 +325,14 @@ async def async_set_swing_mode(self, swing_mode): @property def swing_mode(self) -> Optional[str]: """The current swing mode of the fan.""" - if ( - self._prop_horizontal_swing is None - and self._prop_vertical_swing is None - ): + if (self._prop_horizontal_swing is None and + self._prop_vertical_swing is None): return None - horizontal: bool = ( - self.get_prop_value(prop=self._prop_horizontal_swing) - if self._prop_horizontal_swing else False) - vertical: bool = ( - self.get_prop_value(prop=self._prop_vertical_swing) - if self._prop_vertical_swing else False) + horizontal: bool = (self.get_prop_value( + prop=self._prop_horizontal_swing) + if self._prop_horizontal_swing else False) + vertical: bool = (self.get_prop_value(prop=self._prop_vertical_swing) + if self._prop_vertical_swing else False) if horizontal and vertical: return SWING_BOTH elif horizontal: @@ -375,9 +347,8 @@ class FeatureTemperature(MIoTServiceEntity, ClimateEntity): """Temperature of the climate entity.""" _prop_env_temperature: Optional[MIoTSpecProperty] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the feature class.""" self._prop_env_temperature = None @@ -390,18 +361,16 @@ def __init__( @property def current_temperature(self) -> Optional[float]: """The current environment temperature.""" - return ( - self.get_prop_value(prop=self._prop_env_temperature) - if self._prop_env_temperature else None) + return (self.get_prop_value(prop=self._prop_env_temperature) + if self._prop_env_temperature else None) class FeatureHumidity(MIoTServiceEntity, ClimateEntity): """Humidity of the climate entity.""" _prop_env_humidity: Optional[MIoTSpecProperty] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the feature class.""" self._prop_env_humidity = None @@ -414,18 +383,16 @@ def __init__( @property def current_humidity(self) -> Optional[float]: """The current environment humidity.""" - return ( - self.get_prop_value(prop=self._prop_env_humidity) - if self._prop_env_humidity else None) + return (self.get_prop_value( + prop=self._prop_env_humidity) if self._prop_env_humidity else None) class FeatureTargetHumidity(MIoTServiceEntity, ClimateEntity): """TARGET_HUMIDITY feature of the climate entity.""" _prop_target_humidity: Optional[MIoTSpecProperty] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the feature class.""" self._prop_target_humidity = None @@ -450,29 +417,22 @@ async def async_set_humidity(self, humidity): humidity = self._attr_max_humidity elif humidity < self._attr_min_humidity: humidity = self._attr_min_humidity - await self.set_property_async( - prop=self._prop_target_humidity, value=humidity) + await self.set_property_async(prop=self._prop_target_humidity, + value=humidity) @property def target_humidity(self) -> Optional[int]: """The current target humidity.""" - return ( - self.get_prop_value(prop=self._prop_target_humidity) - if self._prop_target_humidity else None) - - -class Heater( - FeatureOnOff, - FeatureTargetTemperature, - FeatureTemperature, - FeatureHumidity, - FeaturePresetMode -): + return (self.get_prop_value(prop=self._prop_target_humidity) + if self._prop_target_humidity else None) + + +class Heater(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature, + FeatureHumidity, FeaturePresetMode): """Heater""" - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the heater.""" super().__init__(miot_device=miot_device, entity_data=entity_data) @@ -489,29 +449,21 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @property def hvac_mode(self) -> Optional[HVACMode]: """The current hvac mode.""" - return ( - HVACMode.HEAT if self.get_prop_value(prop=self._prop_on) - else HVACMode.OFF) - - -class AirConditioner( - FeatureOnOff, - FeatureTargetTemperature, - FeatureTargetHumidity, - FeatureTemperature, - FeatureHumidity, - FeatureFanMode, - FeatureSwingMode -): + return (HVACMode.HEAT if self.get_prop_value( + prop=self._prop_on) else HVACMode.OFF) + + +class AirConditioner(FeatureOnOff, FeatureTargetTemperature, + FeatureTargetHumidity, FeatureTemperature, FeatureHumidity, + FeatureFanMode, FeatureSwingMode): """Air conditioner""" _prop_mode: Optional[MIoTSpecProperty] _hvac_mode_map: Optional[dict[int, HVACMode]] _prop_ac_state: Optional[MIoTSpecProperty] _value_ac_state: Optional[dict[str, int]] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the air conditioner.""" self._prop_mode = None self._hvac_mode_map = None @@ -524,8 +476,7 @@ def __init__( for prop in entity_data.props: if prop.name == 'mode': if not prop.value_list: - _LOGGER.error( - 'invalid mode value_list, %s', self.entity_id) + _LOGGER.error('invalid mode value_list, %s', self.entity_id) continue self._hvac_mode_map = {} for item in prop.value_list.items: @@ -546,8 +497,8 @@ def __init__( elif prop.name == 'ac-state': self._prop_ac_state = prop self._value_ac_state = {} - self.sub_prop_changed( - prop=prop, handler=self.__ac_state_changed) + self.sub_prop_changed(prop=prop, + handler=self.__ac_state_changed) if self._attr_hvac_modes is None: self._attr_hvac_modes = [HVACMode.OFF] @@ -558,25 +509,22 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the target hvac mode.""" # set the device off if hvac_mode == HVACMode.OFF: - if not await self.set_property_async( - prop=self._prop_on, value=False - ): - raise RuntimeError( - f'set climate prop.on failed, {hvac_mode}, ' - f'{self.entity_id}') + if not await self.set_property_async(prop=self._prop_on, + value=False): + raise RuntimeError(f'set climate prop.on failed, {hvac_mode}, ' + f'{self.entity_id}') return # set the device on if self.get_prop_value(prop=self._prop_on) is False: - await self.set_property_async(prop=self._prop_on, value=True, + await self.set_property_async(prop=self._prop_on, + value=True, write_ha_state=False) # set mode if self._prop_mode is None: return - mode_value = self.get_map_key( - map_=self._hvac_mode_map, value=hvac_mode) + mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode) if mode_value is None or not await self.set_property_async( - prop=self._prop_mode, value=mode_value - ): + prop=self._prop_mode, value=mode_value): raise RuntimeError( f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}') @@ -585,11 +533,10 @@ def hvac_mode(self) -> Optional[HVACMode]: """The current hvac mode.""" if self.get_prop_value(prop=self._prop_on) is False: return HVACMode.OFF - return ( - self.get_map_value( - map_=self._hvac_mode_map, - key=self.get_prop_value(prop=self._prop_mode)) - if self._prop_mode else None) + return (self.get_map_value(map_=self._hvac_mode_map, + key=self.get_prop_value( + prop=self._prop_mode)) + if self._prop_mode else None) def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: del prop @@ -619,55 +566,41 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None: 4: HVACMode.DRY, }.get(v_ac_state['M'], None) if mode: - self.set_prop_value( - prop=self._prop_mode, - value=self.get_map_key( - map_=self._hvac_mode_map, value=mode)) + self.set_prop_value(prop=self._prop_mode, + value=self.get_map_key( + map_=self._hvac_mode_map, value=mode)) # T: target temperature if 'T' in v_ac_state and self._prop_target_temp: - self.set_prop_value( - prop=self._prop_target_temp, value=v_ac_state['T']) + self.set_prop_value(prop=self._prop_target_temp, + value=v_ac_state['T']) # S: fan level. 0: auto, 1: low, 2: media, 3: high if 'S' in v_ac_state and self._prop_fan_level: - self.set_prop_value( - prop=self._prop_fan_level, value=v_ac_state['S']) + self.set_prop_value(prop=self._prop_fan_level, + value=v_ac_state['S']) # D: swing mode. 0: on, 1: off - if ( - 'D' in v_ac_state - and self._attr_swing_modes - and len(self._attr_swing_modes) == 2 - ): - if ( - SWING_HORIZONTAL in self._attr_swing_modes - and self._prop_horizontal_swing - ): - self.set_prop_value( - prop=self._prop_horizontal_swing, value=v_ac_state[ - 'D'] == 0) - elif ( - SWING_VERTICAL in self._attr_swing_modes - and self._prop_vertical_swing - ): - self.set_prop_value( - prop=self._prop_vertical_swing, value=v_ac_state['D'] == 0) + if ('D' in v_ac_state and self._attr_swing_modes and + len(self._attr_swing_modes) == 2): + if (SWING_HORIZONTAL in self._attr_swing_modes and + self._prop_horizontal_swing): + self.set_prop_value(prop=self._prop_horizontal_swing, + value=v_ac_state['D'] == 0) + elif (SWING_VERTICAL in self._attr_swing_modes and + self._prop_vertical_swing): + self.set_prop_value(prop=self._prop_vertical_swing, + value=v_ac_state['D'] == 0) self._value_ac_state.update(v_ac_state) _LOGGER.debug('ac_state update, %s', self._value_ac_state) -class PtcBathHeater( - FeatureTargetTemperature, - FeatureTemperature, - FeatureFanMode, - FeatureSwingMode -): +class PtcBathHeater(FeatureTargetTemperature, FeatureTemperature, + FeatureFanMode, FeatureSwingMode): """Ptc bath heater""" _prop_mode: Optional[MIoTSpecProperty] _hvac_mode_map: Optional[dict[int, HVACMode]] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the ptc bath heater.""" self._prop_mode = None self._hvac_mode_map = None @@ -678,8 +611,7 @@ def __init__( for prop in entity_data.props: if prop.name == 'mode': if not prop.value_list: - _LOGGER.error( - 'invalid mode value_list, %s', self.entity_id) + _LOGGER.error('invalid mode value_list, %s', self.entity_id) continue self._hvac_mode_map = {} for item in prop.value_list.items: @@ -702,38 +634,29 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the target hvac mode.""" if self._prop_mode is None: return - mode_value = self.get_map_key( - map_=self._hvac_mode_map, value=hvac_mode) + mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode) if mode_value is None or not await self.set_property_async( - prop=self._prop_mode, value=mode_value - ): + prop=self._prop_mode, value=mode_value): raise RuntimeError( f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}') @property def hvac_mode(self) -> Optional[HVACMode]: """The current hvac mode.""" - return ( - self.get_map_value( - map_=self._hvac_mode_map, - key=self.get_prop_value(prop=self._prop_mode)) - if self._prop_mode else None) - - -class Thermostat( - FeatureOnOff, - FeatureTargetTemperature, - FeatureTemperature, - FeatureHumidity, - FeatureFanMode -): + return (self.get_map_value(map_=self._hvac_mode_map, + key=self.get_prop_value( + prop=self._prop_mode)) + if self._prop_mode else None) + + +class Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature, + FeatureHumidity, FeatureFanMode): """Thermostat""" _prop_mode: Optional[MIoTSpecProperty] _hvac_mode_map: Optional[dict[int, HVACMode]] - def __init__( - self, miot_device: MIoTDevice, entity_data: MIoTEntityData - ) -> None: + def __init__(self, miot_device: MIoTDevice, + entity_data: MIoTEntityData) -> None: """Initialize the thermostat.""" self._prop_mode = None self._hvac_mode_map = None @@ -744,8 +667,7 @@ def __init__( for prop in entity_data.props: if prop.name == 'mode': if not prop.value_list: - _LOGGER.error( - 'invalid mode value_list, %s', self.entity_id) + _LOGGER.error('invalid mode value_list, %s', self.entity_id) continue self._hvac_mode_map = {} for item in prop.value_list.items: @@ -773,12 +695,10 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the target hvac mode.""" # set the device off if hvac_mode == HVACMode.OFF: - if not await self.set_property_async( - prop=self._prop_on, value=False - ): - raise RuntimeError( - f'set climate prop.on failed, {hvac_mode}, ' - f'{self.entity_id}') + if not await self.set_property_async(prop=self._prop_on, + value=False): + raise RuntimeError(f'set climate prop.on failed, {hvac_mode}, ' + f'{self.entity_id}') return # set the device on elif self.get_prop_value(prop=self._prop_on) is False: @@ -786,12 +706,9 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # set mode if self._prop_mode is None: return - mode_value = self.get_map_key( - map_=self._hvac_mode_map, value=hvac_mode - ) + mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode) if mode_value is None or not await self.set_property_async( - prop=self._prop_mode, value=mode_value - ): + prop=self._prop_mode, value=mode_value): raise RuntimeError( f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}') @@ -800,8 +717,7 @@ def hvac_mode(self) -> Optional[HVACMode]: """The current hvac mode.""" if self.get_prop_value(prop=self._prop_on) is False: return HVACMode.OFF - return ( - self.get_map_value( - map_=self._hvac_mode_map, - key=self.get_prop_value(prop=self._prop_mode)) - if self._prop_mode else None) + return (self.get_map_value(map_=self._hvac_mode_map, + key=self.get_prop_value( + prop=self._prop_mode)) + if self._prop_mode else None) From ae1839bf9aa10825e06077b82e01896cd2669784 Mon Sep 17 00:00:00 2001 From: LiShuzhen Date: Thu, 23 Jan 2025 20:48:26 +0800 Subject: [PATCH 11/11] fix: initialize _attr_hvac_modes --- custom_components/xiaomi_home/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index ba7619e1..1af617a3 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -473,6 +473,7 @@ def __init__(self, miot_device: MIoTDevice, super().__init__(miot_device=miot_device, entity_data=entity_data) self._attr_icon = 'mdi:air-conditioner' # hvac modes + self._attr_hvac_modes = None for prop in entity_data.props: if prop.name == 'mode': if not prop.value_list: @@ -664,6 +665,7 @@ def __init__(self, miot_device: MIoTDevice, super().__init__(miot_device=miot_device, entity_data=entity_data) self._attr_icon = 'mdi:thermostat' # hvac modes + self._attr_hvac_modes = None for prop in entity_data.props: if prop.name == 'mode': if not prop.value_list: