Skip to content

Commit

Permalink
feat: add bath-heater as climate entity
Browse files Browse the repository at this point in the history
  • Loading branch information
SusanPhevos committed Dec 26, 2024
1 parent 80931aa commit 694858e
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 0 deletions.
238 changes: 238 additions & 0 deletions custom_components/xiaomi_home/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
24 changes: 24 additions & 0 deletions custom_components/xiaomi_home/miot/specs/specv2entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
}

Expand Down

0 comments on commit 694858e

Please sign in to comment.