Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support xiaomi heater devices #135

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading