flyminos's picture
download
raw
14.1 kB
"""Support for Xiaomi binary sensors."""
import logging
import time
import json
from datetime import datetime
from functools import cached_property
from homeassistant.components.binary_sensor import (
DOMAIN as ENTITY_DOMAIN,
BinarySensorEntity as BaseEntity,
BinarySensorDeviceClass,
)
from homeassistant.helpers.restore_state import RestoreEntity
from . import (
DOMAIN,
CONF_MODEL,
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
HassEntry,
XEntity,
MiotToggleEntity,
async_setup_config_entry,
bind_services_to_entries,
)
from .core.miot_spec import (
MiotSpec,
MiotService,
)
from .core.xiaomi_cloud import MiotCloud
from .core.utils import local_zone
_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
SERVICE_TO_METHOD = {}
async def async_setup_entry(hass, config_entry, async_add_entities):
HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
hass.data.setdefault(DATA_KEY, {})
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
config['hass'] = hass
did = str(config.get('miot_did') or '')
model = str(config.get(CONF_MODEL) or '')
spec = hass.data[DOMAIN]['miot_specs'].get(model)
entities = []
if isinstance(spec, MiotSpec):
for srv in spec.get_services(
'toilet', 'seat', 'motion_sensor', 'magnet_sensor', 'submersion_sensor', 'alertor',
):
if spec.get_service('nobody_time'):
# lumi.motion.agl02
# lumi.motion.agl04
pass
elif model in ['lumi.sensor_wleak.aq1', 'htcx.alarm.dt210']:
pass
elif not srv.mapping():
continue
if srv.name in ['toilet']:
entities.append(MiotToiletEntity(config, srv))
elif srv.name in ['seat'] and spec.name in ['toilet']:
if spec.get_service('toilet'):
continue
# tinymu.toiletlid.v1
entities.append(MiotToiletEntity(config, srv))
elif 'blt.' in did:
entities.append(BleBinarySensorEntity(config, srv))
elif 'lumi.' in model:
entities.append(LumiBinarySensorEntity(config, srv))
else:
entities.append(MiotBinarySensorEntity(config, srv))
for entity in entities:
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
async_add_entities(entities, update_before_add=True)
bind_services_to_entries(hass, SERVICE_TO_METHOD)
class BinarySensorEntity(XEntity, BaseEntity, RestoreEntity):
def set_state(self, data: dict):
val = self.conv.value_from_dict(data)
if val is None:
return
if self.custom_reverse:
self._attr_extra_state_attributes['reverse_state'] = True
val = not val
self._attr_is_on = val
def get_state(self) -> dict:
return {self.attr: not self._attr_is_on if self.custom_reverse else self._attr_is_on}
@cached_property
def custom_reverse(self):
return self.custom_config_bool('reverse_state', False)
XEntity.CLS[ENTITY_DOMAIN] = BinarySensorEntity
class MiotBinarySensorEntity(MiotToggleEntity, BaseEntity):
def __init__(self, config, miot_service: MiotService, **kwargs):
kwargs.setdefault('logger', _LOGGER)
super().__init__(miot_service, config=config, **kwargs)
pls = []
if len(miot_service.properties) > 0:
first_property = list(miot_service.properties.values() or [])[0].name
if first_property:
pls.append(first_property)
self._prop_state = miot_service.get_property(*pls)
if miot_service.name in ['motion_sensor']:
self._prop_state = miot_service.get_property('motion_state', 'no_motion_duration') or self._prop_state
if self._prop_state.name in ['illumination']:
# cgllc.motion.cgpr1
self._prop_state = None
self._attr_device_class = BinarySensorDeviceClass.MOTION
if self._prop_state is None:
srv = miot_service.spec.get_service('nobody_time')
if srv:
self._prop_state = srv.get_property('nobody_time')
if miot_service.name in ['magnet_sensor']:
self._prop_state = miot_service.get_property('contact_state') or self._prop_state
if self._prop_state and self._prop_state.name in ['contact_state']:
# https://github.com/al-one/hass-xiaomi-miot/issues/270
self._vars['reverse_state'] = True
self._attr_device_class = BinarySensorDeviceClass.DOOR
if miot_service.name in ['submersion_sensor']:
self._prop_state = miot_service.get_property('submersion_state') or self._prop_state
self._attr_device_class = BinarySensorDeviceClass.MOISTURE
async def async_added_to_hass(self):
await super().async_added_to_hass()
if prop := self.custom_config('state_property'):
self._prop_state = self._miot_service.get_property_by_full_name(prop) or self._prop_state
self._state_attrs.update({
'state_property': self._prop_state.full_name if self._prop_state else None,
})
rev = self.custom_config_bool('reverse_state', None)
if rev is not None:
self._vars['reverse_state'] = rev
@property
def is_on(self):
ret = self._state
if self._prop_state:
val = self._prop_state.from_device(self.device)
if val is None:
pass
elif self._prop_state.name in ['no_motion_duration', 'nobody_time']:
if self._prop_state.unit in ['minutes']:
val *= 60
dur = self.custom_config_integer('motion_timeout')
if dur is None and self._prop_state.value_range:
stp = self._prop_state.range_step()
if stp >= 10:
dur = self._prop_state.range_min() + stp
if dur is None:
dur = 60
ret = val <= dur
elif self._prop_state.value_list:
# linp.magnet.m1
if des := self._prop_state.list_description(val):
des = f'{des}'.lower()
return des in ['open', 'opened']
else:
ret = val
if self._vars.get('reverse_state'):
ret = not ret
return ret
@property
def device_class(self):
return self.get_device_class(BinarySensorDeviceClass)
class BleBinarySensorEntity(MiotBinarySensorEntity):
def __init__(self, config, miot_service: MiotService):
super().__init__(config, miot_service)
self._prop_illumination = miot_service.get_property('illumination')
if not self._prop_illumination:
if srv := miot_service.spec.get_service('illumination_sensor'):
self._prop_illumination = srv.get_property('illumination')
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition
self._props = [
'event.15', # 0x000F motion&illumination
'prop.4103', # 0x1007 lux
'prop.4106', # 0x100A battery
'prop.4117', # 0x1015 smoke
'prop.4119', # 0x1017 no_motion_duration
'prop.4123', # 0x101B no_motion_timeout
'prop.4120', # 0x1018 illumination_level
'prop.4121', # 0x1019 magnet
]
async def async_update_for_main_entity(self):
if self.custom_config_bool('use_ble_object', True):
await self.async_update_ble_data()
await super().async_update_for_main_entity()
async def async_update_ble_data(self):
did = self.miot_did
mic = self.miot_cloud
if not did or not isinstance(mic, MiotCloud):
return
pms = {
'did': did,
'props': self._props,
}
rdt = await mic.async_request_api('device/batchdevicedatas', [pms]) or {}
self.logger.debug('%s: Got miio cloud props: %s', self.name_model, rdt)
props = (rdt.get('result') or {}).get(did, {})
sta = None
adt = {}
for k, v in props.items():
if v is None:
continue
ise = 'event.' in k
evt = json.loads(v) if ise else {'value': [v]}
tim = float(evt.get('timestamp') or 0)
val = vlk = None
if vls := evt.get('value'):
val = vls[0]
if val:
try:
val = int.from_bytes(bytes.fromhex(val), 'little')
except (TypeError, ValueError):
val = None
self.logger.warning('%s: BLE object data invalid: %s (%s)', self.name_model, k, vls)
if ise and not tim:
continue
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E6%9C%89%E4%BA%BA%E7%A7%BB%E5%8A%A8%E4%BA%8B%E4%BB%B6%EF%BC%88%E5%B8%A6%E5%85%89%E7%85%A7%EF%BC%89
if k == 'event.15':
adt.update({
'trigger_time': tim,
'trigger_at': datetime.fromtimestamp(tim, local_zone()),
})
dif = time.time() - adt['trigger_time']
sta = dif <= (self.custom_config_integer('motion_timeout') or 60)
vlk = 'illumination'
if self._prop_illumination and self._prop_illumination.value_range:
vlk = self._prop_illumination.full_name
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E5%85%89%E7%85%A7%E5%BA%A6%E5%B1%9E%E6%80%A7
elif k == 'prop.4103':
vlk = 'illumination'
if self._prop_illumination and self._prop_illumination.value_range:
vlk = self._prop_illumination.full_name
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E6%97%A0%E4%BA%BA%E7%A7%BB%E5%8A%A8%E5%B1%9E%E6%80%A7
elif k == 'prop.4119':
vlk = 'no_motion_seconds'
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E5%85%89%E7%85%A7%E5%BC%BA%E5%BC%B1%E5%B1%9E%E6%80%A7
elif k == 'prop.4120':
adt['light_strong'] = not not val
vlk = 'illumination_level'
val = 'strong' if val else 'weak'
if self._prop_illumination and self._prop_illumination.value_list:
vid = self._prop_illumination.list_value(val)
if vid is not None:
adt[self._prop_illumination.full_name] = vid
# https://iot.mi.com/new/doc/embedded-development/ble/object-definition#%E9%97%A8%E7%A3%81%E5%B1%9E%E6%80%A7
elif k == 'prop.4121':
sta = val != 2
if vlk is not None and val is not None:
adt[vlk] = val
if sta is not None:
self._state = sta
if adt:
await self.async_update_attrs(adt)
class MiotToiletEntity(MiotBinarySensorEntity):
def __init__(self, config, miot_service: MiotService):
super().__init__(config, miot_service)
self._prop_state = None
for s in miot_service.spec.get_services('toilet', 'seat'):
if p := s.get_property('seating_state'):
self._prop_state = p
break
if not self._prop_state:
self._prop_state = miot_service.get_property(
self._prop_state.name if self._prop_state else 'status',
)
@property
def icon(self):
return 'mdi:toilet'
class LumiBinarySensorEntity(MiotBinarySensorEntity):
def __init__(self, config, miot_service: MiotService):
super().__init__(config, miot_service)
async def async_update(self):
await super().async_update()
if not self._available:
return
mic = self.miot_cloud
pes = dlg = None
if isinstance(mic, MiotCloud):
now = int(time.time())
ofs = self.custom_config_integer('time_start_offset') or -86400 * 3
dlg = await mic.async_get_last_device_data(self.miot_did, 'device_log', time_start=now + ofs)
pes = json.loads(dlg or '[]')
adt = {}
typ = None
dif = time.time()
if pes and len(pes) >= 2:
typ = pes[1][0]
adt['trigger_type'] = typ
adt['trigger_time'] = int(pes[0] or 0)
adt['trigger_at'] = f'{datetime.fromtimestamp(adt["trigger_time"])}'
dif = time.time() - adt['trigger_time']
if typ == 'prop.illumination':
prop = self._miot_service.get_property('illumination')
if prop:
adt[prop.full_name] = pes[1][1][0]
self._state = None
if typ == 'event.motion' or self._miot_service.name in ['motion_sensor']:
self._state = dif <= (self.custom_config_integer('motion_timeout') or 60)
elif typ in ['event.open', 'event.close']:
self._state = typ == 'event.open'
elif typ in ['event.leak', 'event.no_leak']:
self._state = typ == 'event.leak'
elif self._prop_state and self._prop_state.full_name in self._state_attrs:
_LOGGER.info('%s: Get miio data failed: %s', self.name_model, dlg)
else:
_LOGGER.warning('%s: Get miio data failed: %s', self.name_model, dlg)
if self._prop_state and self._state is not None:
adt[self._prop_state.full_name] = self._state
if adt:
await self.async_update_attrs(adt)

Xet Storage Details

Size:
14.1 kB
·
Xet hash:
32d5316228141f3add182fb21f3af0dd8d1b24cf17b0a042fefbb07054669b59

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.