Skip to content

Commit

Permalink
Merge pull request #135 from XiaoMi/feat_heater_devices
Browse files Browse the repository at this point in the history
feat: support xiaomi heater devices
  • Loading branch information
SusanPhevos authored Dec 18, 2024
2 parents ce4f23b + 6e2de89 commit 10aa78f
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 18 deletions.
162 changes: 153 additions & 9 deletions custom_components/xiaomi_home/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -344,31 +347,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:
Expand All @@ -377,7 +380,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.
Expand All @@ -387,7 +390,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.
Expand Down Expand Up @@ -473,3 +476,144 @@ 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]
_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:
"""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
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:
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 == '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)

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

@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]:
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)
4 changes: 2 additions & 2 deletions custom_components/xiaomi_home/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions custom_components/xiaomi_home/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_home/miot/miot_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions custom_components/xiaomi_home/miot/miot_lan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 25 additions & 2 deletions custom_components/xiaomi_home/miot/specs/specv2entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', 'heat-level'}
},
}
},
'optional': {
'environment': {
'required': {},
'optional': {
'properties': {'temperature', 'relative-humidity'}
}
},
},
'entity': 'heater'
}
}

"""SPEC_SERVICE_TRANS_MAP
Expand Down

0 comments on commit 10aa78f

Please sign in to comment.