Skip to content

Commit

Permalink
❄️ refactor for climate
Browse files Browse the repository at this point in the history
  • Loading branch information
al-one committed Feb 8, 2025
1 parent f80b569 commit 7f1a8ba
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 18 deletions.
1 change: 1 addition & 0 deletions custom_components/xiaomi_miot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@ def __init__(self, name, device, **kwargs):
self._config = dict(kwargs.get('config') or {})
self.device = self._config.get(CONF_DEVICE)
self.hass = self.device.hass
self.log = self.device.log
self.logger = self.device.log
self._miio_info = self.device.info.miio_info
self._unique_did = self.unique_did
Expand Down
248 changes: 232 additions & 16 deletions custom_components/xiaomi_miot/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
)
from homeassistant.components.climate import (
DOMAIN as ENTITY_DOMAIN,
ClimateEntity,
ClimateEntityFeature, # v2022.5
ClimateEntity as BaseEntity,
ClimateEntityFeature,
SWING_ON,
SWING_OFF,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.helpers.restore_state import RestoreEntity
Expand All @@ -31,6 +33,7 @@
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiotEntity,
MiotToggleEntity,
async_setup_config_entry,
Expand Down Expand Up @@ -69,8 +72,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services(
ENTITY_DOMAIN, 'air_conditioner', 'air_condition_outlet',
'ir_aircondition_control', 'thermostat', 'heater', 'ptc_bath_heater',
ENTITY_DOMAIN, 'air_condition_outlet',
'ir_aircondition_control', 'ptc_bath_heater',
):
if srv.name in ['ir_aircondition_control']:
entities.append(MiirClimateEntity(config, srv))
Expand All @@ -89,7 +92,37 @@ class SwingModes(Enum):
both = 3


class BaseClimateEntity(MiotEntity, ClimateEntity):
class BaseClimateEntity(BaseEntity):
_hvac_modes = None
_attr_is_on = None
_attr_device_class = None
_attr_preset_mode = None
_attr_swing_mode = None
_attr_swing_modes = None
_attr_swing_horizontal_mode = None
_attr_swing_horizontal_modes = None
_attr_temperature_unit = None

def on_init(self):
self._hvac_modes = {
HVACMode.OFF: {'list': ['Off', 'Idle', 'None'], 'action': HVACAction.OFF},
HVACMode.AUTO: {'list': ['Auto', 'Manual', 'Normal']},
HVACMode.COOL: {'list': ['Cool'], 'action': HVACAction.COOLING},
HVACMode.HEAT: {'list': ['Heat'], 'action': HVACAction.HEATING},
HVACMode.DRY: {'list': ['Dry'], 'action': HVACAction.DRYING},
HVACMode.FAN_ONLY: {'list': ['Fan'], 'action': HVACAction.FAN},
}

def prop_temperature_unit(self, prop: MiotProperty):
if prop:
if prop.unit in ['celsius', UnitOfTemperature.CELSIUS]:
return UnitOfTemperature.CELSIUS
if prop.unit in ['fahrenheit', UnitOfTemperature.FAHRENHEIT]:
return UnitOfTemperature.FAHRENHEIT
if prop.unit in ['kelvin', UnitOfTemperature.KELVIN]:
return UnitOfTemperature.KELVIN
return UnitOfTemperature.CELSIUS

def update_bind_sensor(self):
bss = self.custom_config_list('bind_sensor') or []
ext = {}
Expand All @@ -104,7 +137,7 @@ def update_bind_sensor(self):
num = float(sta.state)
except ValueError:
num = None
_LOGGER.info('%s: Got bound state from %s: %s, state invalid', self.name_model, bse, sta.state)
self.log.info('Got bound state from %s: %s, state invalid', bse, sta.state)
if num is not None:
cls = sta.attributes.get(ATTR_DEVICE_CLASS)
unit = sta.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
Expand All @@ -114,7 +147,197 @@ def update_bind_sensor(self):
ext[ATTR_CURRENT_HUMIDITY] = num
if ext:
self.update_attrs(ext)
_LOGGER.debug('%s: Got bound state from %s: %s', self.name_model, bss, ext)
self.log.debug('Got bound state from %s: %s', bss, ext)


class ClimateEntity(XEntity, BaseClimateEntity):
_conv_power = None
_conv_mode = None
_conv_speed = None
_conv_swing = None
_conv_swing_h = None
_conv_target_temp = None
_conv_current_temp = None
_conv_current_humidity = None
_prop_temperature = None

def on_init(self):
BaseClimateEntity.on_init(self)

if self._miot_service:
if prop := self.custom_config('current_temp_property'):
self._prop_temperature = self._miot_service.spec.get_property(prop)

for attr in self.conv.attrs:
conv = self.device.find_converter(attr)
prop = getattr(conv, 'prop', None) if conv else None
if not isinstance(prop, MiotProperty):
continue
elif prop.in_list(['on']):
self._conv_power = conv
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
self._attr_supported_features |= ClimateEntityFeature.TURN_OFF
elif prop.in_list(['mode']):
self._conv_mode = conv
self._attr_hvac_modes = []
self._attr_preset_modes = prop.list_descriptions()
remove_hvac_modes = []
for mk, mv in self._hvac_modes.items():
val = prop.list_first(*(mv.get('list') or []))
if val is not None:
des = prop.list_description(val)
self._hvac_modes[mk]['value'] = val
self._hvac_modes[mk]['description'] = des
self._attr_preset_modes.remove(des)
elif mk != HVACMode.OFF:
remove_hvac_modes.append(mk)
for mk in remove_hvac_modes:
self._hvac_modes.pop(mk, None)
for mk in self._hvac_modes.keys():
self._attr_hvac_modes.append(mk)
if self._attr_preset_modes:
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
elif prop.in_list(['fan_level', 'speed_level', 'heat_level']):
self._conv_speed = conv
self._attr_fan_modes = prop.list_descriptions()
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
elif prop.in_list(['vertical_swing']):
self._conv_swing = conv
self._attr_swing_modes = [SWING_ON, SWING_OFF]
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
elif prop.in_list(['horizontal_swing']):
self._conv_swing_h = conv
self._attr_swing_horizontal_modes = [SWING_ON, SWING_OFF]
self._attr_supported_features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
elif prop.in_list(['target_temperature']):
self._conv_target_temp = conv
self._attr_min_temp = prop.range_min()
self._attr_max_temp = prop.range_max()
self._attr_target_temperature_step = prop.range_step()
self._attr_temperature_unit = self.prop_temperature_unit(prop)
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
elif prop.in_list(['indoor_temperature', 'temperature']):
self._conv_current_temp = conv
if not self._attr_temperature_unit:
self._attr_temperature_unit = self.prop_temperature_unit(prop)
elif prop.in_list(['relative_humidity', 'humidity']):
self._conv_current_humidity = conv

def set_state(self, data: dict):
if self._conv_mode:
val = self._conv_mode.value_from_dict(data)
if val in self._attr_preset_modes:
self._attr_preset_mode = val
elif val is not None:
for mk, mv in self._hvac_modes.items():
if val == mv.get('description'):
self._attr_hvac_mode = mk
break
if self._conv_power:
val = self._conv_power.value_from_dict(data)
self._attr_is_on = val
if val in [False, 0]:
self._attr_hvac_mode = HVACMode.OFF
self._attr_state = self._attr_hvac_mode

if self._conv_speed:
val = self._conv_speed.value_from_dict(data)
if val is not None:
self._attr_fan_mode = val
if self._conv_swing:
val = self._conv_swing.value_from_dict(data)
if val is not None:
self._attr_swing_mode = SWING_ON if val else SWING_OFF
if self._conv_swing_h:
val = self._conv_swing_h.value_from_dict(data)
if val is not None:
self._attr_swing_horizontal_mode = SWING_ON if val else SWING_OFF

self.update_bind_sensor()
if self._conv_target_temp:
val = self._conv_target_temp.value_from_dict(data)
if val is not None:
self._attr_target_temperature = val
if self._conv_current_temp:
val = self._conv_current_temp.value_from_dict(data)
if val is not None:
self._attr_current_temperature = val
if self._conv_current_humidity:
val = self._conv_current_humidity.value_from_dict(data)
if val is not None:
self._attr_current_humidity = val

def update_attrs(self, attrs):
temp = attrs.get(ATTR_CURRENT_TEMPERATURE)
if temp is not None:
self._attr_current_temperature = temp
humi = attrs.get(ATTR_CURRENT_HUMIDITY)
if humi is not None:
self._attr_current_humidity = humi

async def async_turn_on(self):
if self._conv_power:
await self.device.async_write({self._conv_power.attr: True})
return
await super().async_turn_on()

async def async_turn_off(self):
if self._conv_power:
await self.device.async_write({self._conv_power.attr: False})
return
await super().async_turn_off()

async def async_set_hvac_mode(self, hvac_mode: HVACMode):
await self.async_set_temperature(**{ATTR_HVAC_MODE: hvac_mode})

async def async_set_preset_mode(self, preset_mode: str):
if not self._conv_mode:
return
for mk, mv in self._hvac_modes.items():
des = mv.get('description')
if preset_mode == des:
await self.device.async_write({self._conv_mode.attr: des})
return

async def async_set_temperature(self, **kwargs):
dat = {}
hvac = kwargs.get(ATTR_HVAC_MODE)

if self._conv_power:
if hvac == HVACMode.OFF:
await self.device.async_write({self._conv_power.attr: False})
return
if not self._attr_is_on:
dat[self._conv_power.attr] = True

if hvac and hvac != self._attr_hvac_mode and self._conv_mode:
mode = self._hvac_modes.get(hvac)
if not mode:
self.log.warning('Unsupported hvac mode: %s', hvac)
else:
dat[self._conv_mode.attr] = mode.get('description')

temp = kwargs.get(ATTR_TEMPERATURE)
if temp and self._conv_target_temp:
dat[self._conv_target_temp.attr] = temp
await self.device.async_write(dat)

async def async_set_fan_mode(self, fan_mode: str):
if not self._conv_speed:
return
await self.device.async_write({self._conv_speed.attr: fan_mode})

async def async_set_swing_mode(self, swing_mode: str):
if not self._conv_swing:
return
await self.device.async_write({self._conv_swing.attr: swing_mode == SWING_ON})

async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str):
if not self._conv_swing_h:
return
await self.device.async_write({self._conv_swing_h.attr: swing_horizontal_mode == SWING_ON})

XEntity.CLS[ENTITY_DOMAIN] = ClimateEntity


class MiotClimateEntity(MiotToggleEntity, BaseClimateEntity):
Expand Down Expand Up @@ -455,14 +678,7 @@ def set_preset_mode(self, mode: str):
@property
def temperature_unit(self):
prop = self._prop_temperature or self._prop_target_temp
if prop:
if prop.unit in ['celsius', UnitOfTemperature.CELSIUS]:
return UnitOfTemperature.CELSIUS
if prop.unit in ['fahrenheit', UnitOfTemperature.FAHRENHEIT]:
return UnitOfTemperature.FAHRENHEIT
if prop.unit in ['kelvin', UnitOfTemperature.KELVIN]:
return UnitOfTemperature.KELVIN
return UnitOfTemperature.CELSIUS
return self.prop_temperature_unit(prop)

@property
def current_temperature(self):
Expand Down Expand Up @@ -732,7 +948,7 @@ def set_preset_mode(self, preset_mode):
return self.call_parent('set_fan_mode', preset_mode)


class MiirClimateEntity(BaseClimateEntity, RestoreEntity):
class MiirClimateEntity(MiotEntity, BaseClimateEntity, RestoreEntity):
def __init__(self, config: dict, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)
self._available = True
Expand Down
9 changes: 9 additions & 0 deletions custom_components/xiaomi_miot/core/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,15 @@ def __post_init__(self):
self.main_props = ['on', 'fan_level']
super().__post_init__()

@dataclass
class MiotClimateConv(MiotServiceConv):
domain: str = 'climate'

def __post_init__(self):
if not self.main_props:
self.main_props = ['mode']
super().__post_init__()

@dataclass
class MiotCoverConv(MiotServiceConv):
domain: str = 'cover'
Expand Down
17 changes: 15 additions & 2 deletions custom_components/xiaomi_miot/core/device_customizes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2245,8 +2245,7 @@

'*.aircondition.*': {
'sensor_properties': 'electricity.electricity',
'switch_properties': 'air_conditioner.on,uv,heater,eco,dryer,sleep_mode,soft_wind,'
'horizontal_swing,vertical_swing,alarm.alarm',
'switch_properties': 'air_conditioner.on,horizontal_swing,vertical_swing',
'select_properties': 'fan_level',
'number_properties': 'target_humidity',
'fan_services': 'air_fresh',
Expand Down Expand Up @@ -2706,6 +2705,20 @@
{'props': ['mode', 'fan_control.mode'], 'desc': True},
],
},
{
'class': MiotClimateConv,
'services': ['air_conditioner', 'heater', 'thermostat'],
'converters' : [
{'props': ['on', 'target_temperature']},
{'props': ['indoor_temperature', 'temperature']},
{'props': ['environment.indoor_temperature', 'environment.temperature']},
{'props': ['mode', 'fan_control.mode'], 'desc': True},
{'props': ['fan_level', 'fan_control.fan_level', 'heat_level'], 'desc': True},
{'props': ['horizontal_swing', 'fan_control.horizontal_swing']},
{'props': ['vertical_swing', 'fan_control.vertical_swing']},
{'props': ['uv', 'heater', 'eco', 'dryer', 'sleep_mode', 'soft_wind'], 'domain': 'switch'},
],
},
{
'class': MiotCoverConv,
'services': ['airer', 'curtain', 'window_opener', 'motor_controller'],
Expand Down

0 comments on commit 7f1a8ba

Please sign in to comment.