From 99e654f0c7285ab959980a1c36a272192afe2604 Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 10:34:07 +0800 Subject: [PATCH 1/3] style: remove invalid space --- custom_components/xiaomi_home/climate.py | 14 +++++++------- custom_components/xiaomi_home/config_flow.py | 4 ++-- custom_components/xiaomi_home/light.py | 4 ++-- custom_components/xiaomi_home/miot/miot_client.py | 2 +- custom_components/xiaomi_home/miot/miot_lan.py | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index cc12373..909fee4 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -344,31 +344,31 @@ async def async_set_fan_mode(self, fan_mode): f'set climate prop.fan_mode failed, {fan_mode}, ' f'{self.entity_id}') - @ property + @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 + @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 + @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 + @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 - @ property + @property def hvac_mode(self) -> Optional[HVACMode]: """Return the hvac mode. e.g., heat, cool mode.""" if self.get_prop_value(prop=self._prop_on) is False: @@ -377,7 +377,7 @@ def hvac_mode(self) -> Optional[HVACMode]: map_=self._hvac_mode_map, key=self.get_prop_value(prop=self._prop_mode)) - @ property + @property def fan_mode(self) -> Optional[str]: """Return the fan mode. @@ -387,7 +387,7 @@ def fan_mode(self) -> Optional[str]: map_=self._fan_mode_map, key=self.get_prop_value(prop=self._prop_fan_level)) - @ property + @property def swing_mode(self) -> Optional[str]: """Return the swing mode. diff --git a/custom_components/xiaomi_home/config_flow.py b/custom_components/xiaomi_home/config_flow.py index ccc91f9..c1e4e67 100644 --- a/custom_components/xiaomi_home/config_flow.py +++ b/custom_components/xiaomi_home/config_flow.py @@ -564,8 +564,8 @@ async def display_device_filter_form(self, reason: str): last_step=False, ) - @ staticmethod - @ callback + @staticmethod + @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, ) -> config_entries.OptionsFlow: diff --git a/custom_components/xiaomi_home/light.py b/custom_components/xiaomi_home/light.py index 610882e..49229f5 100644 --- a/custom_components/xiaomi_home/light.py +++ b/custom_components/xiaomi_home/light.py @@ -236,7 +236,7 @@ def color_temp_kelvin(self) -> Optional[int]: """Return the color temperature.""" return self.get_prop_value(prop=self._prop_color_temp) - @ property + @property def rgb_color(self) -> Optional[tuple[int, int, int]]: """Return the rgb color value.""" rgb = self.get_prop_value(prop=self._prop_color) @@ -247,7 +247,7 @@ def rgb_color(self) -> Optional[tuple[int, int, int]]: b = rgb & 0xFF return r, g, b - @ property + @property def effect(self) -> Optional[str]: """Return the current mode.""" return self.__get_mode_description( diff --git a/custom_components/xiaomi_home/miot/miot_client.py b/custom_components/xiaomi_home/miot/miot_client.py index 771de41..31c87f0 100644 --- a/custom_components/xiaomi_home/miot/miot_client.py +++ b/custom_components/xiaomi_home/miot/miot_client.py @@ -1760,7 +1760,7 @@ def __request_show_devices_changed_notify( delay_sec, self.__show_devices_changed_notify) -@ staticmethod +@staticmethod async def get_miot_instance_async( hass: HomeAssistant, entry_id: str, entry_data: Optional[dict] = None, persistent_notify: Optional[Callable[[str, str, str], None]] = None diff --git a/custom_components/xiaomi_home/miot/miot_lan.py b/custom_components/xiaomi_home/miot/miot_lan.py index 4635572..525be2f 100644 --- a/custom_components/xiaomi_home/miot/miot_lan.py +++ b/custom_components/xiaomi_home/miot/miot_lan.py @@ -564,11 +564,11 @@ def __init__( 0, lambda: self._main_loop.create_task( self.init_async())) - @ property + @property def virtual_did(self) -> str: return self._virtual_did - @ property + @property def mev(self) -> MIoTEventLoop: return self._mev From b5f9e931b70cf4013eebc1aa83923d7ef92d7cdd Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 10:35:59 +0800 Subject: [PATCH 2/3] feat: support xiaomi heater devices --- custom_components/xiaomi_home/climate.py | 112 +++++++++++++++++- .../xiaomi_home/miot/specs/specv2entity.py | 27 ++++- 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index 909fee4..ccda2b6 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -82,9 +82,12 @@ async def async_setup_entry( new_entities = [] for miot_device in device_list: - for data in miot_device.entity_list.get('climate', []): + for data in miot_device.entity_list.get('air-conditioner', []): new_entities.append( 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)) if new_entities: async_add_entities(new_entities) @@ -115,7 +118,7 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity): def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: - """Initialize the Climate.""" + """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) @@ -473,3 +476,108 @@ def __ac_state_changed(self, prop: MIoTSpecProperty, value: any) -> None: self._value_ac_state.update(v_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] + # service: environment + _prop_env_temp: Optional[MIoTSpecProperty] + _prop_env_humi: Optional[MIoTSpecProperty] + + 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._prop_on = None + self._prop_mode = None + self._prop_target_temp = None + self._prop_env_temp = None + self._prop_env_humi = None + + # properties + 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) + 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 == '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) + + 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) + + @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 + + @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) diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 5c64083..084cb1a 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -208,9 +208,32 @@ } } }, - 'entity': 'climate' + 'entity': 'air-conditioner' }, - 'air-condition-outlet': 'air-conditioner' + 'air-condition-outlet': 'air-conditioner', + 'heater': { + 'required': { + 'heater': { + 'required': { + 'properties': { + 'on': {'read', 'write'} + } + }, + 'optional': { + 'properties': {'target-temperature'} + }, + } + }, + 'optional': { + 'environment': { + 'required': {}, + 'optional': { + 'properties': {'temperature', 'relative-humidity'} + } + }, + }, + 'entity': 'heater' + } } """SPEC_SERVICE_TRANS_MAP From 6e2de896c39731844a9cc04847ff949a62cb70d0 Mon Sep 17 00:00:00 2001 From: topsworld Date: Wed, 18 Dec 2024 17:42:40 +0800 Subject: [PATCH 3/3] feat: update xiaomi heater ctrl logic --- custom_components/xiaomi_home/climate.py | 36 +++++++++++++++++++ .../xiaomi_home/miot/specs/specv2entity.py | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/custom_components/xiaomi_home/climate.py b/custom_components/xiaomi_home/climate.py index ccda2b6..106abb9 100644 --- a/custom_components/xiaomi_home/climate.py +++ b/custom_components/xiaomi_home/climate.py @@ -484,10 +484,13 @@ class Heater(MIoTServiceEntity, ClimateEntity): _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] + _heat_level_map: Optional[dict[int, str]] + def __init__( self, miot_device: MIoTDevice, entity_data: MIoTEntityData ) -> None: @@ -495,12 +498,15 @@ def __init__( 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 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 # properties for prop in entity_data.props: @@ -523,6 +529,21 @@ def __init__( 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': @@ -557,6 +578,13 @@ async def async_set_temperature(self, **kwargs): 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.""" @@ -581,3 +609,11 @@ def hvac_mode(self) -> Optional[HVACMode]: return ( HVACMode.HEAT if self.get_prop_value(prop=self._prop_on) else HVACMode.OFF) + + @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 084cb1a..4a57867 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -220,7 +220,7 @@ } }, 'optional': { - 'properties': {'target-temperature'} + 'properties': {'target-temperature', 'heat-level'} }, } },