Skip to content

Commit

Permalink
↕️ add option target_position_properties (#2080)
Browse files Browse the repository at this point in the history
  • Loading branch information
al-one committed Dec 18, 2024
1 parent fa09f01 commit 320cdae
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 30 deletions.
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=

class BinarySensorEntity(XEntity, BaseEntity, RestoreEntity):
def set_state(self, data: dict):
val = data.get(self.attr)
val = self.conv.value_from_dict(data)
if val is None:
return
if self.custom_reverse:
Expand Down
3 changes: 2 additions & 1 deletion custom_components/xiaomi_miot/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,10 @@ async def async_step_customizing(self, user_input=None):
'switch_properties': cv.string,
'number_properties': cv.string,
'select_properties': cv.string,
'button_properties': cv.string,
'target_position_properties': cv.string,
'sensor_attributes': cv.string,
'binary_sensor_attributes': cv.string,
'button_properties': cv.string,
'button_actions': cv.string,
'select_actions': cv.string,
'text_actions': cv.string,
Expand Down
42 changes: 38 additions & 4 deletions custom_components/xiaomi_miot/core/converters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING, Any
from dataclasses import dataclass
from homeassistant.util import color
from homeassistant.util import color, percentage
from miio.utils import (
rgb_to_int,
int_to_rgb,
Expand Down Expand Up @@ -29,9 +29,18 @@ def with_option(self, **kwargs):
self.option.update(kwargs)
return self

@property
def full_name(self):
if not self.domain:
return self.attr
return f'{self.domain}.{self.attr}'

def value_from_dict(self, data):
return data.get(self.full_name, data.get(self.attr))

# to hass
def decode(self, device: 'Device', payload: dict, value):
payload[self.attr] = value
payload[self.full_name] = value

# from hass
def encode(self, device: 'Device', payload: dict, value):
Expand Down Expand Up @@ -68,7 +77,7 @@ def decode(self, device: 'Device', payload: dict, value):
payload.update({
**infos,
**device.props,
'converters': [c.attr for c in device.converters],
'converters': [c.full_name for c in device.converters],
'customizes': device.customizes,
**infos,
})
Expand Down Expand Up @@ -111,7 +120,10 @@ def decode(self, device: 'Device', payload: dict, value):
def encode(self, device: 'Device', payload: dict, value):
if self.prop:
if self.desc:
value = self.prop.list_value(value)
if isinstance(value, list):
value = self.prop.list_first(value)
else:
value = self.prop.list_value(value)
elif self.prop.is_integer:
value = int(value) # bool to int
super().encode(device, payload, value)
Expand Down Expand Up @@ -239,3 +251,25 @@ def encode(self, device: 'Device', payload: dict, value: tuple):
rgb = color.color_hs_to_RGB(*value)
num = rgb_to_int(rgb)
super().encode(device, payload, num)

@dataclass
class PercentagePropConv(MiotPropConv):
ranged = None

def __post_init__(self):
super().__post_init__()
if self.prop and self.prop.value_range:
self.ranged = (self.prop.range_min(), self.prop.range_max())

def decode(self, device: 'Device', payload: dict, value: int):
if self.ranged:
value = int(percentage.ranged_value_to_percentage(self.ranged, value))
super().decode(device, payload, value)

def encode(self, device: 'Device', payload: dict, value: int):
if self.ranged:
value = int(percentage.percentage_to_ranged_value(self.ranged, value))
super().encode(device, payload, value)

class MiotTargetPositionConv(PercentagePropConv):
pass
38 changes: 27 additions & 11 deletions custom_components/xiaomi_miot/core/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
)
from .hass_entry import HassEntry
from .hass_entity import XEntity, BasicEntity, convert_unique_id
from .converters import BaseConv, InfoConv, MiotPropConv, MiotPropValueConv, MiotActionConv, AttrConv
from .converters import (
BaseConv, InfoConv, MiotPropConv,
MiotPropValueConv, MiotActionConv,
AttrConv, MiotTargetPositionConv,
)
from .coordinator import DataCoordinator
from .miot_spec import MiotSpec, MiotProperty, MiotResults, MiotResult
from .miio2miot import Miio2MiotHelper
Expand Down Expand Up @@ -334,12 +338,17 @@ def hass_device_disabled(self):
def add_converter(self, conv: BaseConv):
if conv in self.converters:
return
for c in self.converters:
if c.attr == conv.attr:
self.log.info('Converter for %s already exists. Ignored.', c.attr)
return
if self.find_converter(conv.full_name):
self.log.info('Converter for %s already exists. Ignored.', c.full_name)
return
self.converters.append(conv)

def find_converter(self, full_name):
for c in self.converters:
if c.full_name == full_name:
return c
return None

def init_converters(self):
self.add_converter(InfoConverter)
self.dispatch_info()
Expand Down Expand Up @@ -380,11 +389,11 @@ def init_converters(self):
ac = c(attr, domain=d, prop=prop, desc=pc.get('desc'))
self.add_converter(ac)
if conv and not d:
conv.attrs.add(attr)
conv.attrs.add(ac.full_name)

for d in [
'button', 'sensor', 'binary_sensor', 'switch', 'number', 'select', 'text',
'number_select', 'scanner',
'number_select', 'scanner', 'target_position',
]:
pls = self.custom_config_list(f'{d}_properties') or []
if not pls:
Expand All @@ -401,6 +410,7 @@ def init_converters(self):
platform = {
'scanner': 'device_tracker',
'tracker': 'device_tracker',
'target_position': 'cover',
}.get(d) or d
if platform == 'button':
if prop.value_list:
Expand All @@ -416,8 +426,14 @@ def init_converters(self):
elif platform == 'number' and not prop.value_range:
self.log.warning(f'Unsupported customize entity: %s for %s', platform, prop.full_name)
continue
elif d == 'target_position' and not prop.value_range:
self.log.warning(f'Unsupported customize entity: %s for %s', d, prop.full_name)
continue
else:
conv = MiotPropConv(prop.full_name, platform, prop=prop)
conv_cls = {
'target_position': MiotTargetPositionConv,
}.get(d) or MiotPropConv
conv = conv_cls(prop.full_name, platform, prop=prop)
conv.with_option(
entity_type=None if platform == d else d,
)
Expand Down Expand Up @@ -534,7 +550,7 @@ def add_entities(self, domain):
for conv in self.converters:
if conv.domain != domain:
continue
unique = convert_unique_id(conv)
unique = f'{domain}.{convert_unique_id(conv)}'
entity = self.entities.get(unique)
if entity:
continue
Expand Down Expand Up @@ -571,7 +587,7 @@ def remove_listener(self, handler: Callable):

def dispatch(self, data: dict, only_info=False, log=True):
if log:
self.log.info('Device updated: %s', data)
self.log.info('Device updated: %s', {**data, 'only_info': only_info})
for handler in self.listeners:
handler(data, only_info=only_info)

Expand Down Expand Up @@ -619,7 +635,7 @@ def encode(self, value: dict) -> dict:
payload = {}
for k, v in value.items():
for conv in self.converters:
if conv.attr == k:
if conv.full_name == k:
conv.encode(self, payload, v)
return payload

Expand Down
6 changes: 6 additions & 0 deletions custom_components/xiaomi_miot/core/device_customizes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,7 @@
'switch_properties': 'ai_on',
'select_properties': 'mode,hardness,memory_one,memory_two,sleep_lock',
'number_properties': 'lumbar_angle,backrest_angle,leg_rest_angle',
'target_position_properties': 'lumbar_angle,backrest_angle,leg_rest_angle',
},
'qushui.blanket.mj1': {
'chunk_properties': 1,
Expand All @@ -1354,6 +1355,11 @@
'rhj.sensor_occupy.l730a': {
'sensor_properties': 'illumination,no_one_duration,has_someone_duration',
},
'rmt.bed.zhsbed': {
'sensor_properties': 'fault',
'select_properties': 'mode',
'target_position_properties': 'backrest_angle,leg_rest_angle',
},
'roborock.vacuum.*': {
'sensor_attributes': 'props:clean_area,props:clean_time',
'sensor_properties': 'vacuum.status',
Expand Down
20 changes: 12 additions & 8 deletions custom_components/xiaomi_miot/core/hass_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(self, device: 'Device', conv: 'BaseConv'):
self.device = device
self.hass = device.hass
self.conv = conv
self.attr = conv.attr
self.attr = conv.full_name
self.log = device.log

if isinstance(conv, MiotPropConv):
Expand Down Expand Up @@ -95,10 +95,11 @@ def __init__(self, device: 'Device', conv: 'BaseConv'):
self._miot_property = conv.prop

else:
self.entity_id = device.spec.generate_entity_id(self, self.attr, conv.domain)
# self._attr_name = self.attr.replace('_', '').title()
self._attr_translation_key = self.attr
self.entity_id = device.spec.generate_entity_id(self, conv.attr, conv.domain)
# self._attr_name = conv.attr.replace('_', '').title()
self._attr_translation_key = conv.attr
if isinstance(conv, InfoConv):
self.attr = conv.attr
self._attr_available = True

self.listen_attrs = {self.attr} | set(conv.attrs)
Expand Down Expand Up @@ -161,18 +162,21 @@ def on_device_update(self, data: dict, only_info=False):
state_change = False
self._attr_available = self.device.available

if isinstance(self.conv, InfoConv):
if not only_info:
pass
elif isinstance(self.conv, InfoConv):
self._attr_available = True
self._attr_icon = data.get('icon', self._attr_icon)
self._attr_extra_state_attributes.update(data)
elif only_info:
else:
return

if keys := self.listen_attrs & data.keys():
self.set_state(data)
state_change = True
for key in keys:
self._attr_extra_state_attributes[key] = data.get(key)
if conv := self.device.find_converter(key):
self._attr_extra_state_attributes[conv.attr] = self.device.props.get(conv.attr)

if state_change and self.added:
self._async_write_ha_state()
Expand Down Expand Up @@ -218,7 +222,7 @@ def customize_keys(self):
keys.append(f'{mod}:{prop.full_name}')
keys.append(f'{mod}:{prop.name}')
if self.attr and not (prop or action):
keys.append(f'{mod}:{self.attr}')
keys.append(f'{mod}:{self.conv.attr}')
return keys


Expand Down
75 changes: 73 additions & 2 deletions custom_components/xiaomi_miot/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from homeassistant.components.cover import (
DOMAIN as ENTITY_DOMAIN,
CoverEntity,
CoverEntity as BaseEntity,
CoverEntityFeature, # v2022.5
CoverDeviceClass,
ATTR_POSITION,
Expand All @@ -15,14 +15,17 @@
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiotEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
MiotProperty,
)
from .core.converters import MiotPropConv, MiotTargetPositionConv

_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
Expand Down Expand Up @@ -54,7 +57,75 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
bind_services_to_entries(hass, SERVICE_TO_METHOD)


class MiotCoverEntity(MiotEntity, CoverEntity):
class CoverEntity(XEntity, BaseEntity):
_attr_is_closed = None
_attr_target_cover_position = None
_attr_supported_features = CoverEntityFeature(0)
_conv_status = None
_conv_motor: MiotPropConv = None
_conv_current_position = None
_conv_target_position = None
_current_range = None
_target_range = None

def on_init(self):
for conv in self.device.converters:
prop = getattr(conv, 'prop', None)
if not isinstance(prop, MiotProperty):
continue
elif prop.in_list(['status']):
self._conv_status = conv
elif prop.in_list(['motor_control']):
self._conv_motor = conv
elif prop.in_list(['current_position']) and prop.value_range:
self._conv_current_position = conv
self._current_range = (prop.range_min, prop.range_max)
elif prop.value_range and isinstance(conv, MiotTargetPositionConv):
self._conv_target_position = conv
self._target_range = conv.ranged
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
elif prop.value_range and prop.in_list(['target_position']):
self._conv_target_position = conv
self._target_range = (prop.range_min(), prop.range_max())
self._attr_supported_features |= CoverEntityFeature.SET_POSITION

def set_state(self, data: dict):
if self._conv_current_position:
val = self._conv_current_position.value_from_dict(data)
if val is not None:
self._attr_current_cover_position = int(val)
if self._conv_target_position:
val = self._conv_target_position.value_from_dict(data)
if val is not None:
self._attr_target_cover_position = int(val)
if not self._conv_current_position:
self._attr_current_cover_position = self._attr_target_cover_position

async def async_open_cover(self, **kwargs):
if self._conv_motor:
val = self._conv_motor.prop.list_first('Open', 'Up')
if val is not None:
await self.device.async_write({self._conv_motor.full_name: val})
return
await self.async_set_cover_position(100)

async def async_close_cover(self, **kwargs):
if self._conv_motor:
val = self._conv_motor.prop.list_first('Close', 'Down')
if val is not None:
await self.device.async_write({self._conv_motor.full_name: val})
return
await self.async_set_cover_position(0)

async def async_set_cover_position(self, position, **kwargs):
if not self._conv_target_position:
return
await self.device.async_write({self._conv_target_position.full_name: position})

XEntity.CLS[ENTITY_DOMAIN] = CoverEntity


class MiotCoverEntity(MiotEntity, BaseEntity):
def __init__(self, config: dict, miot_service: MiotService):
super().__init__(miot_service, config=config, logger=_LOGGER)

Expand Down
3 changes: 1 addition & 2 deletions custom_components/xiaomi_miot/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ def get_state(self) -> dict:
return {self.attr: self._attr_native_value}

def set_state(self, data: dict):
val = data.get(self.attr)
self._attr_native_value = val
self._attr_native_value = self.conv.value_from_dict(data)

async def async_set_native_value(self, value: float):
await self.device.async_write({self.attr: value})
Expand Down
2 changes: 1 addition & 1 deletion custom_components/xiaomi_miot/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def get_state(self) -> dict:
return {self.attr: self._attr_native_value}

def set_state(self, data: dict):
val = data.get(self.attr)
val = self.conv.value_from_dict(data)
if isinstance(val, list):
val = val[0] if val else None
if val is None:
Expand Down
Loading

0 comments on commit 320cdae

Please sign in to comment.