初始化config文件
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,136 @@
|
||||
"""Support alarm_control_panel entity for Xiaomi Miot."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature, # v2022.5
|
||||
)
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
MiotEntity,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
from .core.miot_spec import (
|
||||
MiotSpec,
|
||||
MiotService,
|
||||
)
|
||||
from .core.const import AlarmControlPanelState
|
||||
|
||||
_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
|
||||
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('arming'):
|
||||
if not srv.get_property('arming_mode'):
|
||||
continue
|
||||
entities.append(MiotAlarmEntity(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 MiotAlarmEntity(MiotEntity, AlarmControlPanelEntity):
|
||||
def __init__(self, config, miot_service: MiotService):
|
||||
super().__init__(miot_service, config=config, logger=_LOGGER)
|
||||
self._attr_code_arm_required = False
|
||||
self._is_mgl03 = self.model == 'lumi.gateway.mgl03'
|
||||
self._prop_mode = miot_service.get_property('arming_mode')
|
||||
if self._prop_mode:
|
||||
if self._prop_mode.list_value('home_arming') is not None:
|
||||
self._supported_features |= AlarmControlPanelEntityFeature.ARM_HOME
|
||||
if self._prop_mode.list_value('away_arming') is not None:
|
||||
self._supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
if self._prop_mode.list_value('sleep_arming') is not None:
|
||||
self._supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
if self._is_mgl03:
|
||||
self._supported_features |= AlarmControlPanelEntityFeature.TRIGGER
|
||||
|
||||
async def async_update(self):
|
||||
await super().async_update()
|
||||
if not self._available:
|
||||
return
|
||||
self.update_state()
|
||||
|
||||
def update_state(self):
|
||||
sta = None
|
||||
if self._prop_mode:
|
||||
val = self._prop_mode.from_device(self.device)
|
||||
des = self._prop_mode.list_description(val) if val is not None else None
|
||||
if des is not None:
|
||||
des = f'{des}'.lower()
|
||||
if 'basic' in des:
|
||||
sta = AlarmControlPanelState.DISARMED
|
||||
elif 'home' in des:
|
||||
sta = AlarmControlPanelState.ARMED_HOME
|
||||
elif 'away' in des:
|
||||
sta = AlarmControlPanelState.ARMED_AWAY
|
||||
elif 'sleep' in des:
|
||||
sta = AlarmControlPanelState.ARMED_NIGHT
|
||||
if self._is_mgl03:
|
||||
if self.device.props.get('arming.alarm'):
|
||||
sta = AlarmControlPanelState.TRIGGERED
|
||||
if hasattr(self, '_attr_alarm_state'):
|
||||
self._attr_alarm_state = sta
|
||||
else:
|
||||
self._attr_state = sta
|
||||
|
||||
async def async_set_arm_mode(self, mode):
|
||||
ret = False
|
||||
val = self._prop_mode.list_value(mode)
|
||||
if val is not None:
|
||||
ret = await self.async_set_property(self._prop_mode, val)
|
||||
if ret:
|
||||
self.update_state()
|
||||
return ret
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
return await self.async_set_arm_mode('basic_arming')
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
return await self.async_set_arm_mode('home_arming')
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
return await self.async_set_arm_mode('away_arming')
|
||||
|
||||
async def async_alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
return await self.async_set_arm_mode('sleep_arming')
|
||||
|
||||
def alarm_arm_vacation(self, code=None):
|
||||
"""Send arm vacation command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
if self._is_mgl03:
|
||||
return await self.async_set_miot_property(3, 22, 1)
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command."""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,343 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Support button entity for Xiaomi Miot."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.button import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
ButtonEntity as BaseEntity,
|
||||
)
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
BaseSubEntity,
|
||||
async_setup_config_entry,
|
||||
)
|
||||
from .core.templates import template
|
||||
|
||||
_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, {})
|
||||
|
||||
|
||||
class ButtonEntity(XEntity, BaseEntity):
|
||||
def on_init(self):
|
||||
self._attr_available = True
|
||||
if des := getattr(self.conv, 'description', None):
|
||||
self._attr_name = f'{self._attr_name} {des}'
|
||||
|
||||
def set_state(self, data: dict):
|
||||
pass
|
||||
|
||||
async def async_press(self):
|
||||
pms = getattr(self.conv, 'value', None)
|
||||
if self._miot_action and self._miot_action.ins:
|
||||
pms = self.custom_config_list('action_params', pms)
|
||||
if pms:
|
||||
vars = {
|
||||
'attrs': self.device.props,
|
||||
}
|
||||
pms = [
|
||||
v if not isinstance(v, str) else template(v, self.hass).async_render(vars)
|
||||
for v in pms
|
||||
]
|
||||
await self.device.async_write({self.attr: pms})
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = ButtonEntity
|
||||
|
||||
|
||||
class ButtonSubEntity(BaseEntity, BaseSubEntity):
|
||||
def __init__(self, parent, attr, option=None):
|
||||
BaseSubEntity.__init__(self, parent, attr, option)
|
||||
self._available = True
|
||||
self._async_action = self._option.get('async_press_action')
|
||||
self._press_action = self._option.get('press_action')
|
||||
self._press_kwargs = {
|
||||
'attr': self._attr,
|
||||
**(self._option.get('press_kwargs') or {}),
|
||||
}
|
||||
self._state_attrs = self._option.get('state_attrs') or {}
|
||||
|
||||
def update(self, data=None):
|
||||
return
|
||||
|
||||
def press(self):
|
||||
"""Press the button."""
|
||||
if not self._press_action:
|
||||
raise NotImplementedError()
|
||||
if ret := self._press_action(**self._press_kwargs):
|
||||
self.schedule_update_ha_state()
|
||||
return ret
|
||||
|
||||
async def async_press(self):
|
||||
if self._async_action:
|
||||
if ret := await self._async_action(**self._press_kwargs):
|
||||
self.schedule_update_ha_state()
|
||||
return ret
|
||||
await super().async_press()
|
||||
@@ -0,0 +1,705 @@
|
||||
"""Support for Xiaomi cameras."""
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import locale
|
||||
import base64
|
||||
import requests
|
||||
import re
|
||||
import collections
|
||||
from os import urandom
|
||||
from functools import partial
|
||||
from urllib.parse import urlencode
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.const import STATE_IDLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.camera import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
Camera,
|
||||
CameraEntityFeature, # v2022.5
|
||||
)
|
||||
from homeassistant.components.ffmpeg import async_get_image, DATA_FFMPEG
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
Device,
|
||||
HassEntry,
|
||||
XEntity,
|
||||
MiotToggleEntity,
|
||||
BaseSubEntity,
|
||||
MiotCloud,
|
||||
MiCloudException,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
from .core.const import CameraState
|
||||
from .core.miot_spec import (
|
||||
MiotSpec,
|
||||
MiotService,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
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
|
||||
model = str(config.get(CONF_MODEL) or '')
|
||||
spec = hass.data[DOMAIN]['miot_specs'].get(model)
|
||||
entities = []
|
||||
if isinstance(spec, MiotSpec):
|
||||
svs = spec.get_services(ENTITY_DOMAIN, 'camera_control', 'video_doorbell')
|
||||
if svs:
|
||||
svs = [] # migrate to converter
|
||||
elif spec.services:
|
||||
srv = None
|
||||
if spec.name in ['video_doorbell']:
|
||||
# loock.cateye.v02
|
||||
srv = spec.get_service('p2p_stream') or spec.first_service()
|
||||
elif model in [
|
||||
'lumi.lock.bmcn05',
|
||||
'lumi.lock.mcn002',
|
||||
'lumi.lock.wbmcn1',
|
||||
'loock.lock.t1pro',
|
||||
]:
|
||||
srv = spec.first_service()
|
||||
if isinstance(srv, MiotService):
|
||||
svs = [srv]
|
||||
for srv in svs:
|
||||
entities.append(MiotCameraEntity(hass, config, srv))
|
||||
for entity in entities:
|
||||
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
|
||||
async_add_entities(entities)
|
||||
bind_services_to_entries(hass, SERVICE_TO_METHOD)
|
||||
|
||||
|
||||
class BaseCameraEntity(Camera):
|
||||
_state_attrs: dict
|
||||
_last_image = None
|
||||
_last_url = None
|
||||
_url_expiration = 0
|
||||
_extra_arguments = None
|
||||
device: Device = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant):
|
||||
super().__init__()
|
||||
self._supported_features = CameraEntityFeature(0)
|
||||
self.access_tokens = collections.deque(self.access_tokens, 12 * 2)
|
||||
self._manager = hass.data.get(DATA_FFMPEG)
|
||||
# http://ffmpeg.org/ffmpeg-all.html
|
||||
self._ffmpeg_options = ''
|
||||
self._segment_iv_hex = urandom(16).hex()
|
||||
self._segment_iv_b64 = base64.b64encode(bytes.fromhex(self._segment_iv_hex)).decode()
|
||||
if not hasattr(self, '_attr_extra_state_attributes'):
|
||||
self._attr_extra_state_attributes = {}
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
return self.device_info.get('manufacturer')
|
||||
|
||||
@property
|
||||
def is_doorbell(self):
|
||||
if '.lock.' in self.model:
|
||||
return True
|
||||
service = getattr(self, '_miot_service', None)
|
||||
if service and service.name in ['video_doorbell']:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def image_source(self, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_camera_image(self, width=None, height=None):
|
||||
url = await self.image_source()
|
||||
if url:
|
||||
if '-i ' not in str(url):
|
||||
url = f'-i "{url}"'
|
||||
self._last_image = await async_get_image(
|
||||
self.hass,
|
||||
f'{self._ffmpeg_options or ""} {url}'.strip(),
|
||||
extra_cmd=self._extra_arguments,
|
||||
width=width,
|
||||
height=height,
|
||||
)
|
||||
return self._last_image
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
if not self.is_on:
|
||||
_LOGGER.debug('%s: camera is off. %s', self.name, self._state_attrs)
|
||||
return
|
||||
url = await self.stream_source()
|
||||
if not url:
|
||||
_LOGGER.debug('%s: stream source is empty. %s', self.name, self._state_attrs)
|
||||
return
|
||||
if '-i ' not in str(url):
|
||||
url = f'-i "{url}"'
|
||||
stream = CameraMjpeg(self._manager.binary)
|
||||
await stream.open_camera(
|
||||
f'{self._ffmpeg_options or ""} {url}'.strip(),
|
||||
extra_cmd=self._extra_arguments,
|
||||
)
|
||||
try:
|
||||
stream_reader = await stream.get_reader()
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass,
|
||||
request,
|
||||
stream_reader,
|
||||
self._manager.ffmpeg_stream_content_type,
|
||||
timeout=60,
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
await stream.close()
|
||||
except BrokenPipeError:
|
||||
_LOGGER.error('%s: Got BrokenPipeError when close stream: %s', self.name, url)
|
||||
|
||||
async def _async_log_stderr_stream(self, stderr_reader):
|
||||
"""Log output from ffmpeg."""
|
||||
while True:
|
||||
line = await stderr_reader.readline()
|
||||
if line == b'':
|
||||
return
|
||||
_LOGGER.info('%s: ffmpeg stderr: %s', self.name, line.rstrip())
|
||||
|
||||
async def get_alarm_playlist(self, begin, ended=None, limit=2):
|
||||
cloud = self.device.cloud
|
||||
if not cloud:
|
||||
return None
|
||||
api = cloud.get_api_by_host('business.smartcamera.api.io.mi.com', 'miot/camera/app/v1/alarm/playlist/limit')
|
||||
rqd = {
|
||||
'did': self.device.did,
|
||||
'region': str(cloud.default_server).upper(),
|
||||
'language': locale.getlocale()[0],
|
||||
'beginTime': begin,
|
||||
'endTime': ended or int(time.time() * 1000 + 999),
|
||||
'limit': limit,
|
||||
}
|
||||
rdt = await cloud.async_request_api(api, rqd, method='GET', crypt=True) or {}
|
||||
rls = rdt.get('data', {}).get('playUnits') or []
|
||||
adt = {}
|
||||
if rls:
|
||||
fst = rls[0] or {}
|
||||
tim = fst.pop('createTime', 0) / 1000
|
||||
adt = {
|
||||
'motion_video_time': f'{datetime.fromtimestamp(tim)}',
|
||||
'motion_video_type': ','.join(fst.get('tags') or []),
|
||||
'motion_video_latest': fst,
|
||||
}
|
||||
else:
|
||||
self.log.warning('Camera alarm playlist is empty. %s', rdt)
|
||||
return adt
|
||||
|
||||
async def get_alarm_eventlist(self, begin, ended=None, doorbell=False, limit=2):
|
||||
cloud = self.device.cloud
|
||||
if not cloud:
|
||||
return None
|
||||
api = cloud.get_api_by_host('business.smartcamera.api.io.mi.com', 'common/app/get/eventlist')
|
||||
rqd = {
|
||||
'did': self.device.did,
|
||||
'model': self.model,
|
||||
'doorBell': doorbell,
|
||||
'eventType': 'Default',
|
||||
'needMerge': True,
|
||||
'sortType': 'DESC',
|
||||
'region': str(cloud.default_server).upper(),
|
||||
'language': locale.getlocale()[0],
|
||||
'beginTime': begin,
|
||||
'endTime': ended or int(time.time() * 1000 + 999),
|
||||
'limit': limit,
|
||||
}
|
||||
rdt = await cloud.async_request_api(api, rqd, method='GET', crypt=True) or {}
|
||||
rls = rdt.get('data', {}).get('thirdPartPlayUnits') or []
|
||||
adt = {}
|
||||
if rls:
|
||||
fst = rls[0] or {}
|
||||
tim = fst.pop('createTime', 0) / 1000
|
||||
adt = {
|
||||
'motion_video_time': f'{datetime.fromtimestamp(tim)}',
|
||||
'motion_video_type': fst.get('eventType'),
|
||||
'motion_video_latest': fst,
|
||||
}
|
||||
else:
|
||||
self.log.info('Camera events is empty. %s', rdt)
|
||||
return adt
|
||||
|
||||
def get_alarm_m3u8_url(self, fileId, isAlarm=False, videoCodec='H265'):
|
||||
cloud = self.device.cloud
|
||||
if not cloud or not fileId:
|
||||
return None
|
||||
pms = {
|
||||
'did': str(self.device.did),
|
||||
'model': self.device_info.get('model'),
|
||||
'fileId': fileId,
|
||||
'isAlarm': not not isAlarm,
|
||||
'videoCodec': videoCodec,
|
||||
}
|
||||
api = cloud.get_api_by_host('business.smartcamera.api.io.mi.com', 'common/app/m3u8')
|
||||
pms = cloud.rc4_params('GET', api, {'data': cloud.json_encode(pms)})
|
||||
pms['yetAnotherServiceToken'] = cloud.service_token
|
||||
return f'{api}?{urlencode(pms)}'
|
||||
|
||||
def get_alarm_image_address(self, fileId, storeId, crypto=False):
|
||||
cloud = self.device.cloud
|
||||
if not (cloud and fileId and storeId):
|
||||
return None
|
||||
dat = {
|
||||
'did': str(self.device.did),
|
||||
'fileId': fileId,
|
||||
'stoId': storeId,
|
||||
'segmentIv': self._segment_iv_b64,
|
||||
}
|
||||
api = cloud.get_api_by_host('processor.smartcamera.api.io.mi.com', 'miot/camera/app/v1/img')
|
||||
pms = cloud.rc4_params('GET', api, {'data': cloud.json_encode(dat)})
|
||||
pms['yetAnotherServiceToken'] = cloud.service_token
|
||||
url = f'{api}?{urlencode(pms)}'
|
||||
if crypto:
|
||||
key = base64.b64decode(cloud.ssecurity).hex()
|
||||
url = f'-decryption_key {key} -decryption_iv {self._segment_iv_hex} -i "crypto+{url}"'
|
||||
return url
|
||||
|
||||
|
||||
class CameraEntity(XEntity, BaseCameraEntity):
|
||||
_attr_should_poll = True
|
||||
_attr_camera_image = None
|
||||
_attr_stream_source = None
|
||||
_last_motion_time = None
|
||||
|
||||
def on_init(self):
|
||||
BaseCameraEntity.__init__(self, self.hass)
|
||||
self._attr_brand = self.device_info.get('manufacturer')
|
||||
self._attr_model = self.device_info.get('model')
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
if self._attr_should_poll:
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
def get_state(self) -> dict:
|
||||
return {}
|
||||
|
||||
def set_state(self, data: dict):
|
||||
if 'motion_video_latest' in self.device.props:
|
||||
self._attr_available = True
|
||||
self._attr_should_poll = False
|
||||
self.update_motion_video(self.device.props)
|
||||
|
||||
async def image_source(self):
|
||||
return self._attr_camera_image
|
||||
|
||||
async def stream_source(self):
|
||||
return self._attr_stream_source
|
||||
|
||||
def update_motion_video(self, data: dict):
|
||||
tim = data.get('motion_video_time')
|
||||
if self._last_motion_time == tim:
|
||||
return
|
||||
self._last_motion_time = tim
|
||||
self._attr_extra_state_attributes.update({
|
||||
'motion_video_time': tim,
|
||||
'motion_video_type': data.get('motion_video_type'),
|
||||
})
|
||||
if adt := data.get('motion_video_latest'):
|
||||
if fid := adt.get('fileId'):
|
||||
self._attr_camera_image = self.get_alarm_image_address(fid, adt.get('imgStoreId'), True)
|
||||
self._attr_stream_source = self.get_alarm_m3u8_url(fid, adt.get('isAlarm'))
|
||||
self._attr_extra_state_attributes.update({
|
||||
'stream_address': self._attr_stream_source,
|
||||
})
|
||||
|
||||
async def async_update(self):
|
||||
adt = None
|
||||
stm = int(time.time() - 86400 * 7) * 1000
|
||||
if not self.device.cloud:
|
||||
pass
|
||||
elif self.custom_config_bool('miio_event_human_visit_details_template'):
|
||||
await self.device.update_miio_cloud_records()
|
||||
elif self.custom_config_bool('use_alarm_playlist'):
|
||||
adt = await self.get_alarm_playlist(stm)
|
||||
else:
|
||||
adt = await self.get_alarm_eventlist(stm, None, self.is_doorbell)
|
||||
if adt:
|
||||
self.log.debug('Camera alarm data: %s', adt)
|
||||
self._attr_available = True
|
||||
self.update_motion_video(adt)
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = CameraEntity
|
||||
|
||||
|
||||
class MiotCameraEntity(MiotToggleEntity, BaseCameraEntity):
|
||||
_srv_stream = None
|
||||
_act_start_stream = None
|
||||
_act_stop_stream = None
|
||||
_prop_stream_address = None
|
||||
_prop_expiration_time = None
|
||||
_prop_motion_tracking = None
|
||||
_stream_refresh_unsub = None
|
||||
_motion_entity = None
|
||||
_motion_enable = None
|
||||
_use_motion_stream = False
|
||||
_sub_motion_stream = False
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: dict, miot_service: MiotService):
|
||||
super().__init__(miot_service, config=config, logger=_LOGGER)
|
||||
BaseCameraEntity.__init__(self, hass)
|
||||
if self._prop_power:
|
||||
self._supported_features |= CameraEntityFeature.ON_OFF
|
||||
if miot_service:
|
||||
self._prop_motion_tracking = miot_service.bool_property('motion_detection', 'motion_tracking')
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
sls = ['camera_stream_for_google_home', 'camera_stream_for_amazon_alexa']
|
||||
if self.custom_config_bool('use_rtsp_stream'):
|
||||
sls.reverse()
|
||||
for s in sls:
|
||||
if not self._miot_service:
|
||||
break
|
||||
srv = self._miot_service.spec.get_service(s)
|
||||
if not srv:
|
||||
continue
|
||||
act = srv.get_action('start_hls_stream', 'start_rtsp_stream')
|
||||
if act:
|
||||
self._srv_stream = srv
|
||||
self._act_start_stream = act
|
||||
self._act_stop_stream = srv.get_action('stop_stream')
|
||||
self._prop_stream_address = srv.get_property('stream_address')
|
||||
self._prop_expiration_time = srv.get_property('expiration_time')
|
||||
break
|
||||
if self._prop_stream_address:
|
||||
self._supported_features |= CameraEntityFeature.STREAM
|
||||
self._sub_motion_stream = True
|
||||
elif self._miot_service.name in ['camera_control'] or self.is_doorbell:
|
||||
if self.custom_config_bool('use_motion_stream'):
|
||||
pass
|
||||
elif self.custom_config_bool('sub_motion_stream'):
|
||||
pass
|
||||
else:
|
||||
self._use_motion_stream = True
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def state(self): # noqa
|
||||
if self.is_recording:
|
||||
return CameraState.RECORDING
|
||||
if self.is_streaming:
|
||||
return CameraState.STREAMING
|
||||
return STATE_IDLE
|
||||
|
||||
async def async_update(self):
|
||||
self._state_attrs.pop('motion_video_latest', None) # remove
|
||||
await super().async_update()
|
||||
if not self._available:
|
||||
return
|
||||
|
||||
self._motion_enable = self.custom_config_bool('use_motion_stream', self._use_motion_stream)
|
||||
add_cameras = self._add_entities.get(ENTITY_DOMAIN)
|
||||
if not self._motion_entity \
|
||||
and add_cameras \
|
||||
and self.custom_config_bool('sub_motion_stream', self._sub_motion_stream):
|
||||
self._motion_entity = MotionCameraEntity(self, self.hass)
|
||||
self._subs['motion_event'] = self._motion_entity
|
||||
add_cameras([self._motion_entity], update_before_add=True)
|
||||
|
||||
adt = None
|
||||
stm = int(time.time() - 86400 * 7) * 1000
|
||||
if not self._motion_enable and not self._motion_entity:
|
||||
pass
|
||||
elif 'motion_video_latest' in self._state_attrs:
|
||||
adt = {
|
||||
'motion_video_updated': 1,
|
||||
}
|
||||
elif not self.xiaomi_cloud:
|
||||
pass
|
||||
elif self.custom_config_bool('use_alarm_playlist'):
|
||||
adt = await self.get_alarm_playlist(stm)
|
||||
else:
|
||||
adt = await self.get_alarm_eventlist(stm, None, self.is_doorbell)
|
||||
if adt:
|
||||
self._supported_features |= CameraEntityFeature.STREAM
|
||||
await self.async_update_attrs(adt)
|
||||
if self._motion_enable:
|
||||
await self.async_update_attrs(self.motion_event_attributes)
|
||||
if self._motion_entity:
|
||||
await self.hass.async_add_executor_job(self._motion_entity.update)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
if self._prop_power:
|
||||
return self._prop_power.from_device(self.device) and True
|
||||
return True
|
||||
|
||||
async def stream_source(self, **kwargs):
|
||||
fun = self.async_get_stream_address()
|
||||
if self._motion_enable:
|
||||
kwargs['crypto'] = True
|
||||
fun = self.hass.async_add_executor_job(partial(self.get_motion_stream_address, **kwargs))
|
||||
idx = self.custom_config_integer('motion_stream_slice')
|
||||
if idx is not None:
|
||||
kwargs['index'] = idx
|
||||
fun = self.hass.async_add_executor_job(partial(self.get_motion_stream_slice_video), **kwargs)
|
||||
return await fun
|
||||
|
||||
async def image_source(self, **kwargs):
|
||||
if self._motion_enable:
|
||||
kwargs['crypto'] = True
|
||||
return self.get_motion_image_address(**kwargs)
|
||||
return await self.stream_source()
|
||||
|
||||
async def async_get_stream_address(self, **kwargs):
|
||||
now = time.time()
|
||||
if now >= self._url_expiration:
|
||||
self._last_url = None
|
||||
_LOGGER.debug('%s: camera stream: %s expired: %s', self.name_model, self._last_url, self._url_expiration)
|
||||
result = {}
|
||||
if not self._act_start_stream:
|
||||
self.update_attrs({
|
||||
'miot_error': 'Nonsupport start hls/rstp stream via miot-spec',
|
||||
})
|
||||
elif not self._last_url:
|
||||
updater = 'lan'
|
||||
try:
|
||||
vav = self.custom_config_integer('video_attribute')
|
||||
vap = self._srv_stream.get_property('video_attribute')
|
||||
if vav is None and vap and vap.value_list:
|
||||
vav = (vap.value_list.pop(0) or {}).get('value')
|
||||
if self.xiaomi_cloud:
|
||||
if self._act_stop_stream:
|
||||
await self.async_call_action(self._act_stop_stream)
|
||||
result = await self.async_call_action(self._act_start_stream, [] if vav is None else [vav]) or {}
|
||||
updater = 'cloud'
|
||||
if isinstance(result, dict):
|
||||
_LOGGER.debug('%s: Get miot camera stream from %s: %s', self.name_model, updater, result)
|
||||
else:
|
||||
_LOGGER.warning('%s: Get miot camera stream error from %s: %s', self.name_model, updater, result)
|
||||
result = {}
|
||||
except MiCloudException as exc:
|
||||
_LOGGER.error('%s: Get miot camera stream from %s failed: %s', self.name_model, updater, exc)
|
||||
odt = self._act_start_stream.out_results(result.get('out')) or {
|
||||
'stream_address': '',
|
||||
}
|
||||
self._url_expiration = 0
|
||||
if self._prop_expiration_time:
|
||||
self._url_expiration = int(self._prop_expiration_time.from_dict(odt) or 0) / 1000
|
||||
if self._url_expiration:
|
||||
self._url_expiration -= 10
|
||||
else:
|
||||
self._url_expiration = now + 60 * 4.5
|
||||
if self._prop_stream_address:
|
||||
self._last_url = self._prop_stream_address.from_dict(odt)
|
||||
self.schedule_update_ha_state()
|
||||
self.async_check_stream_address(self._last_url)
|
||||
if not kwargs.get('scheduled') or self.custom_config('keep_streaming'):
|
||||
self._schedule_stream_refresh()
|
||||
odt['expire_at'] = f'{datetime.fromtimestamp(self._url_expiration)}'
|
||||
self.update_attrs(odt)
|
||||
self._attr_is_streaming = self._last_url and True
|
||||
if self._attr_is_streaming:
|
||||
self.update_attrs({
|
||||
'miot_error': None,
|
||||
})
|
||||
return self._last_url
|
||||
|
||||
def async_check_stream_address(self, url):
|
||||
if not url or self.custom_config_bool('disable_check_stream'):
|
||||
return False
|
||||
res = requests.head(url)
|
||||
if res.status_code > 200:
|
||||
self.update_attrs({
|
||||
'stream_http_status': res.status_code,
|
||||
'stream_http_reason': res.reason,
|
||||
})
|
||||
_LOGGER.warning(
|
||||
'%s: stream address status invalid: %s (%s)',
|
||||
self.name,
|
||||
res.status_code,
|
||||
res.reason,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _handle_stream_refresh(self, now, *_):
|
||||
self._stream_refresh_unsub = None
|
||||
await self.stream_source(scheduled=True)
|
||||
|
||||
def _schedule_stream_refresh(self):
|
||||
if self._stream_refresh_unsub is not None:
|
||||
self._stream_refresh_unsub()
|
||||
self._stream_refresh_unsub = async_track_point_in_utc_time(
|
||||
self.hass,
|
||||
self._handle_stream_refresh, # noqa
|
||||
datetime.fromtimestamp(self._url_expiration),
|
||||
)
|
||||
|
||||
@property
|
||||
def motion_event_attributes(self):
|
||||
return {
|
||||
'motion_video_time': self._state_attrs.get('motion_video_time'),
|
||||
'motion_video_type': self._state_attrs.get('motion_video_type'),
|
||||
'stream_address': self.get_motion_stream_address(),
|
||||
'image_address': self.get_motion_image_address(),
|
||||
}
|
||||
|
||||
def get_motion_stream_address(self, **kwargs):
|
||||
mic = self.xiaomi_cloud
|
||||
if not mic:
|
||||
_LOGGER.info('%s: camera does not have cloud.', self.name)
|
||||
return None
|
||||
mvd = self._state_attrs.get('motion_video_latest') or {}
|
||||
fid = mvd.get('fileId')
|
||||
if not fid:
|
||||
_LOGGER.info('%s: camera does not have motion file in cloud.', self.name)
|
||||
return None
|
||||
url = self.get_alarm_m3u8_url(fid, mvd.get('isAlarm'))
|
||||
_LOGGER.debug('%s: Got stream url: %s', self.name_model, url)
|
||||
return url
|
||||
|
||||
def get_motion_video_address(self, **kwargs):
|
||||
mic = self.xiaomi_cloud
|
||||
if not mic:
|
||||
_LOGGER.info('%s: camera does not have cloud.', self.name)
|
||||
return None
|
||||
mvd = self._state_attrs.get('motion_video_latest') or {}
|
||||
fid = mvd.get('fileId')
|
||||
vid = mvd.get('videoStoreId')
|
||||
if not fid or not vid:
|
||||
_LOGGER.info('%s: camera does not have motion video in cloud.', self.name)
|
||||
return None
|
||||
dat = {
|
||||
'did': str(self.miot_did),
|
||||
'fileId': fid,
|
||||
'stoId': vid,
|
||||
'segmentIv': self._segment_iv_b64,
|
||||
}
|
||||
api = mic.get_api_by_host('processor.smartcamera.api.io.mi.com', 'miot/camera/app/v1/mp4')
|
||||
pms = mic.rc4_params('GET', api, {'data': mic.json_encode(dat)})
|
||||
pms['yetAnotherServiceToken'] = mic.service_token
|
||||
url = f'{api}?{urlencode(pms)}'
|
||||
_LOGGER.debug('%s: Got video url: %s', self.name_model, url)
|
||||
|
||||
if kwargs.get('debug'):
|
||||
req = requests.get(url)
|
||||
if float(req.headers.get('x-xiaomi-status-code', 200)) >= 400:
|
||||
try:
|
||||
signed_nonce = mic.signed_nonce(pms['_nonce'])
|
||||
rdt = json.loads(MiotCloud.decrypt_data(signed_nonce, req.text).decode())
|
||||
_LOGGER.info('%s: video stream content: %s', self.name_model, rdt)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if kwargs.get('crypto'):
|
||||
key = base64.b64decode(mic.ssecurity).hex()
|
||||
url = f'-decryption_key {key} -decryption_iv {self._segment_iv_hex} -i "crypto+{url}"'
|
||||
return url
|
||||
|
||||
def get_motion_stream_slice_video(self, **kwargs):
|
||||
url = self.get_motion_stream_address()
|
||||
if not url:
|
||||
_LOGGER.info('%s: camera does not have motion stream in cloud.', self.name)
|
||||
return None
|
||||
req = requests.get(url)
|
||||
if float(req.headers.get('x-xiaomi-status-code', 200)) >= 400:
|
||||
_LOGGER.warning('%s: camera motion stream with a failed http code: %s', self.name_model, req)
|
||||
return url
|
||||
aes_key = None
|
||||
aes__iv = None
|
||||
mat = re.search(r'AES-128,\s*URI="?(https?://[^",]+)"?,\s*IV=(?:0x)?(\w+)', req.text)
|
||||
if mat:
|
||||
aes_key, aes__iv = mat.groups()
|
||||
mat = re.findall(r'[\r\n](https?://[^\r\n]+)', req.text)
|
||||
idx = kwargs.get('index', -1)
|
||||
mp4 = mat.pop(idx) if mat else None
|
||||
if mp4 and aes_key:
|
||||
req = requests.get(aes_key)
|
||||
key = req.content.hex()
|
||||
mp4 = f'-decryption_key {key} -decryption_iv {aes__iv} -i "crypto+{mp4}"'
|
||||
_LOGGER.debug('%s: Got video url: %s', self.name_model, mp4)
|
||||
return mp4
|
||||
|
||||
def get_motion_image_address(self, **kwargs):
|
||||
mic = self.xiaomi_cloud
|
||||
if not mic:
|
||||
_LOGGER.info('%s: camera does not have cloud.', self.name)
|
||||
return None
|
||||
mvd = self._state_attrs.get('motion_video_latest') or {}
|
||||
fid = mvd.get('fileId')
|
||||
iid = mvd.get('imgStoreId')
|
||||
if not fid or not iid:
|
||||
_LOGGER.info('%s: camera does not have motion image in cloud.', self.name)
|
||||
return None
|
||||
dat = {
|
||||
'did': str(self.miot_did),
|
||||
'fileId': fid,
|
||||
'stoId': iid,
|
||||
'segmentIv': self._segment_iv_b64,
|
||||
}
|
||||
api = mic.get_api_by_host('processor.smartcamera.api.io.mi.com', 'miot/camera/app/v1/img')
|
||||
pms = mic.rc4_params('GET', api, {'data': mic.json_encode(dat)})
|
||||
pms['yetAnotherServiceToken'] = mic.service_token
|
||||
url = f'{api}?{urlencode(pms)}'
|
||||
_LOGGER.debug('%s: Got image url: %s', self.name_model, url)
|
||||
|
||||
if kwargs.get('crypto'):
|
||||
key = base64.b64decode(mic.ssecurity).hex()
|
||||
url = f'-decryption_key {key} -decryption_iv {self._segment_iv_hex} -i "crypto+{url}"'
|
||||
return url
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
if self._prop_motion_tracking:
|
||||
return self._prop_motion_tracking.from_device(self.device)
|
||||
return None
|
||||
|
||||
async def async_enable_motion_detection(self):
|
||||
if self._prop_motion_tracking:
|
||||
return await self.async_set_property(self._prop_motion_tracking, True)
|
||||
return False
|
||||
|
||||
async def async_disable_motion_detection(self):
|
||||
if self._prop_motion_tracking:
|
||||
return await self.async_set_property(self._prop_motion_tracking, False)
|
||||
return False
|
||||
|
||||
|
||||
class MotionCameraEntity(BaseSubEntity, BaseCameraEntity):
|
||||
def __init__(self, parent, hass: HomeAssistant, option=None):
|
||||
super().__init__(parent, 'motion_event', option, domain=ENTITY_DOMAIN)
|
||||
BaseCameraEntity.__init__(self, hass)
|
||||
self._available = True
|
||||
self._supported_features |= CameraEntityFeature.STREAM
|
||||
|
||||
def update(self, data=None):
|
||||
super().update(data)
|
||||
self._available = not not self.parent_attributes.get('motion_video_latest')
|
||||
if not self._available:
|
||||
return
|
||||
self.update_attrs(self._parent.motion_event_attributes, update_parent=False)
|
||||
|
||||
async def stream_source(self, **kwargs):
|
||||
kwargs['crypto'] = True
|
||||
return await self.hass.async_add_executor_job(
|
||||
partial(self._parent.get_motion_stream_address, **kwargs)
|
||||
)
|
||||
|
||||
async def image_source(self, **kwargs):
|
||||
kwargs['crypto'] = True
|
||||
return self._parent.get_motion_image_address(**kwargs)
|
||||
@@ -0,0 +1,533 @@
|
||||
"""Support for Xiaomi Aircondition."""
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_FAN_MODE,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
ClimateEntity as BaseEntity,
|
||||
ClimateEntityFeature,
|
||||
SWING_ON,
|
||||
SWING_OFF,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
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,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
|
||||
DEFAULT_MIN_TEMP = 16.0
|
||||
DEFAULT_MAX_TEMP = 31.0
|
||||
|
||||
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
|
||||
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('ir_aircondition_control'):
|
||||
if srv.name in ['ir_aircondition_control']:
|
||||
entities.append(MiirClimateEntity(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 SwingModes(Enum):
|
||||
off = 0
|
||||
vertical = 1
|
||||
horizontal = 2
|
||||
both = 3
|
||||
|
||||
|
||||
class BaseClimateEntity(BaseEntity):
|
||||
_hvac_modes = None
|
||||
_attr_is_on = None
|
||||
_attr_device_class = None
|
||||
_attr_fan_mode = None
|
||||
_attr_fan_modes = None
|
||||
_attr_hvac_mode = None
|
||||
_attr_preset_mode = None
|
||||
_attr_swing_mode = None
|
||||
_attr_swing_modes = None
|
||||
_attr_swing_horizontal_mode = None
|
||||
_attr_swing_horizontal_modes = None
|
||||
_attr_max_temp = None
|
||||
_attr_min_temp = None
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
def on_init(self):
|
||||
self._attr_hvac_modes = []
|
||||
self._hvac_modes = {
|
||||
HVACMode.OFF: {'list': ['Off', 'Idle', 'None'], 'action': HVACAction.OFF},
|
||||
HVACMode.AUTO: {'list': ['Auto', 'Manual', 'Normal'], 'action': HVACAction.IDLE},
|
||||
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 = {}
|
||||
for bse in bss:
|
||||
bse = f'{bse}'.strip()
|
||||
if not bse:
|
||||
continue
|
||||
sta = self.hass.states.get(bse)
|
||||
if not sta or not sta.state or sta.state == STATE_UNKNOWN:
|
||||
continue
|
||||
try:
|
||||
num = float(sta.state)
|
||||
except ValueError:
|
||||
num = None
|
||||
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)
|
||||
if cls == SensorDeviceClass.TEMPERATURE.value or unit in [
|
||||
UnitOfTemperature.CELSIUS,
|
||||
UnitOfTemperature.KELVIN,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
]:
|
||||
ext[ATTR_CURRENT_TEMPERATURE] = self.hass.config.units.temperature(num, unit)
|
||||
elif cls == SensorDeviceClass.HUMIDITY.value:
|
||||
ext[ATTR_CURRENT_HUMIDITY] = num
|
||||
if ext:
|
||||
self.update_attrs(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_target_humidity = None
|
||||
_conv_current_humidity = None
|
||||
_prop_temperature_name = None
|
||||
|
||||
def on_init(self):
|
||||
BaseClimateEntity.on_init(self)
|
||||
|
||||
self._prop_temperature_name = self.custom_config('current_temp_property') or 'indoor_temperature'
|
||||
|
||||
hvac_modes = set()
|
||||
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
|
||||
hvac_modes.add(HVACMode.OFF)
|
||||
hvac_modes.add(HVACMode.AUTO)
|
||||
elif prop.in_list(['mode']):
|
||||
self._conv_mode = conv
|
||||
self._attr_preset_modes = prop.list_descriptions()
|
||||
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)
|
||||
hvac_modes.add(mk)
|
||||
self._hvac_modes[mk]['value'] = val
|
||||
self._hvac_modes[mk]['description'] = des
|
||||
if mk != HVACMode.OFF:
|
||||
self._attr_preset_modes.remove(des)
|
||||
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(lower=True)
|
||||
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']):
|
||||
if hasattr(ClimateEntityFeature, 'SWING_HORIZONTAL_MODE'): # v2024.12
|
||||
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([self._prop_temperature_name, 'temperature']):
|
||||
if prop.in_list([self._prop_temperature_name]):
|
||||
self._conv_current_temp = conv
|
||||
elif not self._conv_current_temp:
|
||||
self._conv_current_temp = conv
|
||||
else:
|
||||
continue
|
||||
self._attr_temperature_unit = self.prop_temperature_unit(prop)
|
||||
elif prop.in_list(['relative_humidity', 'humidity']):
|
||||
self._conv_current_humidity = conv
|
||||
elif prop.in_list(['target_humidity']):
|
||||
self._conv_target_humidity = conv
|
||||
if prop.value_range:
|
||||
self._attr_min_humidity = prop.range_min()
|
||||
self._attr_max_humidity = prop.range_max()
|
||||
self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY
|
||||
|
||||
self._attr_hvac_modes = list(hvac_modes)
|
||||
|
||||
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
|
||||
if val is not None:
|
||||
for mk, mv in self._hvac_modes.items():
|
||||
if val == mv.get('description'):
|
||||
self._attr_hvac_mode = mk
|
||||
self._attr_hvac_action = mv.get('action')
|
||||
self._attr_preset_mode = None
|
||||
break
|
||||
if self._conv_power:
|
||||
val = self._conv_power.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_is_on = val
|
||||
if val in [False, 0]:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
self._attr_preset_mode = None
|
||||
elif val and self._attr_hvac_mode in [None, HVACMode.OFF]:
|
||||
self._attr_hvac_mode = HVACMode.AUTO
|
||||
if val and self._miot_service.name in ['heater'] and self._attr_hvac_action is None:
|
||||
self._attr_hvac_action = HVACAction.HEATING
|
||||
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_target_humidity:
|
||||
val = self._conv_target_humidity.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_target_humidity = 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.async_turn_switch(True)
|
||||
return
|
||||
await super().async_turn_on()
|
||||
|
||||
async def async_turn_off(self):
|
||||
if self._conv_power:
|
||||
await self.async_turn_switch(False)
|
||||
return
|
||||
await super().async_turn_off()
|
||||
|
||||
async def async_turn_switch(self, state):
|
||||
if self._conv_power:
|
||||
await self.device.async_write({self._conv_power.full_name: state})
|
||||
|
||||
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 self._conv_mode:
|
||||
await self.device.async_write({self._conv_mode.full_name: preset_mode})
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
dat = {}
|
||||
hvac = kwargs.get(ATTR_HVAC_MODE)
|
||||
if hvac == HVACMode.OFF and self._conv_power:
|
||||
await self.async_turn_switch(False)
|
||||
return
|
||||
|
||||
if hvac and self._conv_power and self._attr_is_on is False:
|
||||
dat[self._conv_power.full_name] = 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)
|
||||
elif (desc := mode.get('description')) is not None:
|
||||
dat[self._conv_mode.full_name] = desc
|
||||
if self._conv_target_temp and self._attr_target_temperature and hvac in [HVACMode.HEAT, HVACMode.COOL]:
|
||||
dat[self._conv_target_temp.full_name] = self._attr_target_temperature
|
||||
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temp and self._conv_target_temp:
|
||||
prop = self._conv_target_temp.prop
|
||||
if not isinstance(prop, MiotProperty):
|
||||
pass
|
||||
elif prop.is_integer or prop.range_step() == 1:
|
||||
temp = int(temp)
|
||||
dat[self._conv_target_temp.full_name] = temp
|
||||
|
||||
fan_mode = kwargs.get(ATTR_FAN_MODE)
|
||||
if fan_mode and self._conv_speed:
|
||||
dat[self._conv_speed.full_name] = fan_mode
|
||||
|
||||
await self.device.async_write(dat)
|
||||
|
||||
async def async_set_humidity(self, humidity: int):
|
||||
if not self._conv_target_humidity:
|
||||
return
|
||||
await self.device.async_write({self._conv_target_humidity.full_name: humidity})
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str):
|
||||
if not self._conv_speed:
|
||||
return
|
||||
dat = {
|
||||
ATTR_FAN_MODE: fan_mode,
|
||||
}
|
||||
if self._attr_is_on is False and HVACMode.FAN_ONLY in self.hvac_modes:
|
||||
dat[ATTR_HVAC_MODE] = HVACMode.FAN_ONLY
|
||||
await self.async_set_temperature(**dat)
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str):
|
||||
if not self._conv_swing:
|
||||
return
|
||||
await self.device.async_write({self._conv_swing.full_name: 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.full_name: swing_horizontal_mode == SWING_ON})
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = ClimateEntity
|
||||
|
||||
|
||||
class MiirClimateEntity(MiotEntity, BaseClimateEntity, RestoreEntity):
|
||||
def __init__(self, config: dict, miot_service: MiotService):
|
||||
super().__init__(miot_service, config=config, logger=_LOGGER)
|
||||
self._available = True
|
||||
|
||||
self._act_turn_on = miot_service.get_action('turn_on')
|
||||
self._act_turn_off = miot_service.get_action('turn_off')
|
||||
|
||||
# Add turn on/off features if actions are available
|
||||
if self._act_turn_on:
|
||||
if hasattr(ClimateEntityFeature, 'TURN_ON'): # v2024.2+
|
||||
self._supported_features |= ClimateEntityFeature.TURN_ON
|
||||
if self._act_turn_off:
|
||||
if hasattr(ClimateEntityFeature, 'TURN_OFF'): # v2024.2+
|
||||
self._supported_features |= ClimateEntityFeature.TURN_OFF
|
||||
|
||||
self._attr_hvac_mode = None
|
||||
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},
|
||||
}
|
||||
self._attr_hvac_modes = []
|
||||
self._prop_mode = miot_service.get_property('ir_mode')
|
||||
if self._prop_mode:
|
||||
for mk, mv in self._hvac_modes.items():
|
||||
val = self._prop_mode.list_first(*(mv.get('list') or []))
|
||||
if val is not None:
|
||||
self._hvac_modes[mk]['value'] = val
|
||||
self._attr_hvac_modes.append(mk)
|
||||
elif mk == HVACMode.OFF:
|
||||
self._attr_hvac_modes.append(mk)
|
||||
|
||||
self._attr_current_temperature = None
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
self._attr_max_temp = DEFAULT_MAX_TEMP
|
||||
self._attr_min_temp = DEFAULT_MIN_TEMP
|
||||
self._attr_target_temperature_step = 1
|
||||
self._prop_temperature = miot_service.get_property('ir_temperature')
|
||||
if self._prop_temperature:
|
||||
self._supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
self._attr_temperature_unit = self._prop_temperature.unit_of_measurement
|
||||
self._attr_target_temperature_step = self._prop_temperature.range_step() or 1
|
||||
self._attr_max_temp = self._prop_temperature.range_max() or DEFAULT_MAX_TEMP
|
||||
self._attr_min_temp = self._prop_temperature.range_min() or DEFAULT_MIN_TEMP
|
||||
self._attr_target_temperature_high = self._attr_max_temp
|
||||
self._attr_target_temperature_low = self._attr_min_temp
|
||||
self._attr_target_temperature = int((self._attr_max_temp - self._attr_min_temp) / 1.2) + self._attr_min_temp
|
||||
|
||||
self._fan_modes = {}
|
||||
self._attr_fan_modes = []
|
||||
self._attr_fan_mode = None
|
||||
self._act_fan_down = miot_service.get_action('fan_speed_down')
|
||||
if self._act_fan_down:
|
||||
self._fan_modes[self._act_fan_down.friendly_desc] = self._act_fan_down
|
||||
self._act_fan_up = miot_service.get_action('fan_speed_up')
|
||||
if self._act_fan_up:
|
||||
self._fan_modes[self._act_fan_up.friendly_desc] = self._act_fan_up
|
||||
if self._fan_modes:
|
||||
self._supported_features |= ClimateEntityFeature.FAN_MODE
|
||||
self._attr_fan_modes = list(self._fan_modes.keys())
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if hasattr(self, 'async_get_last_state'):
|
||||
if state := await self.async_get_last_state():
|
||||
self._attr_hvac_mode = state.state
|
||||
if self._attr_hvac_mode not in self._hvac_modes:
|
||||
self._attr_hvac_mode = None
|
||||
|
||||
async def async_update(self):
|
||||
self.update_bind_sensor()
|
||||
if ATTR_CURRENT_TEMPERATURE in self._state_attrs:
|
||||
self._attr_current_temperature = self._state_attrs.get(ATTR_CURRENT_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
return self.hvac_mode not in [None, HVACMode.OFF]
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
if not self._act_turn_on:
|
||||
raise NotImplementedError()
|
||||
if ret := await self.async_call_action(self._act_turn_on):
|
||||
self._attr_hvac_mode = HVACMode.AUTO
|
||||
return ret
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
if not self._act_turn_off:
|
||||
raise NotImplementedError()
|
||||
if ret := await self.async_call_action(self._act_turn_off):
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
return ret
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
return await self.async_turn_off()
|
||||
if not self._prop_mode:
|
||||
raise NotImplementedError()
|
||||
val = self._hvac_modes.get(hvac_mode, {}).get('value')
|
||||
if val is None:
|
||||
return False
|
||||
if ret := await self.async_set_property(self._prop_mode, val):
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
return ret
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""Return the current running hvac operation if supported.
|
||||
Need to be one of HVACAction.*.
|
||||
"""
|
||||
if not self.is_on:
|
||||
return HVACAction.OFF
|
||||
hvac = self.hvac_mode
|
||||
if hvac is None:
|
||||
return HVACAction.IDLE
|
||||
return self._hvac_modes.get(hvac, {}).get('action')
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if ATTR_HVAC_MODE in kwargs:
|
||||
try:
|
||||
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
|
||||
except (Exception, NotImplementedError):
|
||||
pass
|
||||
if not self._prop_temperature:
|
||||
raise NotImplementedError()
|
||||
ret = False
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
val = kwargs.get(ATTR_TEMPERATURE)
|
||||
if ret := await self.async_set_property(self._prop_temperature, val):
|
||||
self._attr_target_temperature = val
|
||||
return ret
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
if not self._attr_fan_modes:
|
||||
raise NotImplementedError()
|
||||
ret = False
|
||||
if act := self._fan_modes.get(fan_mode):
|
||||
ret = await self.async_call_action(act)
|
||||
return ret
|
||||
@@ -0,0 +1,911 @@
|
||||
"""Config flow to configure Xiaomi Miot."""
|
||||
import logging
|
||||
import re
|
||||
import copy
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from typing import Optional
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_TOKEN,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback, split_entity_id
|
||||
from homeassistant.util import yaml
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.selector import ObjectSelector
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
CONF_CONN_MODE,
|
||||
CONF_SERVER_COUNTRY,
|
||||
CONF_CONFIG_VERSION,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_CONN_MODE,
|
||||
init_integration_data,
|
||||
)
|
||||
from .core.utils import (
|
||||
DeviceException,
|
||||
get_customize_via_entity,
|
||||
get_customize_via_model,
|
||||
in_china,
|
||||
async_analytics_track_event,
|
||||
)
|
||||
from .core.const import SUPPORTED_DOMAINS, CLOUD_SERVERS, CONF_XIAOMI_CLOUD, HA_VERSION
|
||||
from .core.device import MiioInfo
|
||||
from .core.miot_spec import MiotSpec
|
||||
from .core.mini_miio import AsyncMiIO
|
||||
from .core.xiaomi_cloud import (
|
||||
MiotCloud,
|
||||
MiCloudException,
|
||||
MiCloudAccessDenied,
|
||||
MiCloudNeedVerify,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_INTERVAL = 30
|
||||
|
||||
# 0.1 support multiple integration to add the same device
|
||||
# 0.2 new entity id format (model_mac[-4:]_suffix)
|
||||
# 0.3 washer modes via select
|
||||
ENTRY_VERSION = 0.3
|
||||
|
||||
CONN_MODES = {
|
||||
'auto': 'Automatic (自动模式)',
|
||||
'local': 'Local (本地模式)',
|
||||
'cloud': 'Cloud (云端模式)',
|
||||
}
|
||||
|
||||
|
||||
async def check_miio_device(hass, user_input, errors):
|
||||
host = user_input.get(CONF_HOST)
|
||||
token = user_input.get(CONF_TOKEN)
|
||||
try:
|
||||
device = AsyncMiIO(host, token)
|
||||
info = await device.info()
|
||||
if info:
|
||||
info = MiioInfo(info)
|
||||
except DeviceException:
|
||||
device = None
|
||||
info = None
|
||||
errors['base'] = 'cannot_connect'
|
||||
_LOGGER.debug('Xiaomi Miot config flow: %s', {
|
||||
'user_input': user_input,
|
||||
'miio_info': info,
|
||||
'errors': errors,
|
||||
})
|
||||
model = ''
|
||||
if info is not None:
|
||||
if not user_input.get(CONF_MODEL):
|
||||
model = str(info.model or '')
|
||||
user_input[CONF_MODEL] = model
|
||||
user_input['miio_info'] = dict(info or {})
|
||||
miot_type = await MiotSpec.async_get_model_type(hass, model)
|
||||
if not miot_type:
|
||||
miot_type = await MiotSpec.async_get_model_type(hass, model, use_remote=True)
|
||||
user_input['miot_type'] = miot_type
|
||||
user_input['unique_did'] = format_mac(info.mac_address)
|
||||
if miot_type and device:
|
||||
try:
|
||||
pms = [
|
||||
{'did': 'miot', 'siid': 2, 'piid': 1},
|
||||
{'did': 'miot', 'siid': 2, 'piid': 2},
|
||||
{'did': 'miot', 'siid': 3, 'piid': 1},
|
||||
]
|
||||
results = await device.send('get_properties', pms) or []
|
||||
for prop in results:
|
||||
if not isinstance(prop, dict):
|
||||
continue
|
||||
if prop.get('code') == 0:
|
||||
# Collect supported models in LAN
|
||||
await async_analytics_track_event(
|
||||
hass, 'miot', 'local', model,
|
||||
firmware=info.firmware_version,
|
||||
results=results,
|
||||
miot_urn=miot_type,
|
||||
ap_rssi=info.get('ap', {}).get('rssi', ''),
|
||||
wifi_fw_ver=info.get('wifi_fw_ver', ''),
|
||||
)
|
||||
break
|
||||
except DeviceException:
|
||||
pass
|
||||
return user_input
|
||||
|
||||
|
||||
class BaseFlowHandler:
|
||||
hass = None
|
||||
context = None
|
||||
config_data = None
|
||||
cloud: Optional[MiotCloud] = None
|
||||
devices: Optional[list] = None
|
||||
|
||||
@property
|
||||
def placeholders(self):
|
||||
return self.context.setdefault('placeholders', {})
|
||||
|
||||
def pop_placeholders(self):
|
||||
return {
|
||||
'tip': '',
|
||||
**self.context.pop('placeholders', {}),
|
||||
}
|
||||
|
||||
async def get_cloud(self, user_input):
|
||||
if not self.cloud:
|
||||
self.cloud = await MiotCloud.from_token(self.hass, user_input, login=False)
|
||||
self.cloud.login_times = 0
|
||||
self.cloud.merger_config(user_input)
|
||||
login_data = {}
|
||||
if verify_ticket := user_input.pop('verify_ticket', None):
|
||||
login_data['verify_ticket'] = verify_ticket
|
||||
if captcha := user_input.pop('captcha', None):
|
||||
login_data['captcha'] = captcha
|
||||
if login_data:
|
||||
await self.cloud.async_login(login_data=login_data)
|
||||
elif not await self.cloud.async_check_auth(notify=False):
|
||||
raise MiCloudException('Login failed')
|
||||
return self.cloud
|
||||
|
||||
async def check_xiaomi_account(self, user_input, errors, renew_devices=False):
|
||||
dvs = []
|
||||
mic = None
|
||||
try:
|
||||
mic = await self.get_cloud(user_input)
|
||||
dvs = await mic.async_get_devices(renew=renew_devices) or []
|
||||
if renew_devices:
|
||||
await MiotSpec.async_get_model_type(self.hass, 'xiaomi.miot.auto', use_remote=True)
|
||||
self.context.pop('captchaIck', None)
|
||||
except (MiCloudException, MiCloudAccessDenied, Exception) as exc:
|
||||
err = f'{exc}'
|
||||
self.placeholders['tip'] = f'⚠️ {err}'
|
||||
errors['base'] = 'cannot_login'
|
||||
if not mic:
|
||||
mic = self.cloud
|
||||
if isinstance(exc, MiCloudNeedVerify) and mic:
|
||||
errors['base'] = exc.message
|
||||
self.context[exc.message] = True
|
||||
self.placeholders.update({
|
||||
'url': exc.url,
|
||||
'tip': f'[打开验证网页 | Open the verification page]({exc.url})',
|
||||
})
|
||||
elif isinstance(exc, MiCloudAccessDenied) and mic:
|
||||
if url := mic.attrs.pop('captchaImg', None):
|
||||
err = f'Captcha:\n'
|
||||
self.placeholders['tip'] = f'⚠️ {err}'
|
||||
self.context['captchaIck'] = mic.attrs.get('captchaIck')
|
||||
elif isinstance(exc, requests.exceptions.ConnectionError):
|
||||
errors['base'] = 'cannot_reach'
|
||||
elif 'ZoneInfoNotFoundError' in err:
|
||||
errors['base'] = 'tzinfo_error'
|
||||
unm = mic.username if mic else user_input.get(CONF_USERNAME)
|
||||
_LOGGER.error('Setup xiaomi cloud for user: %s failed.', unm, exc_info=True)
|
||||
if not errors:
|
||||
self.devices = dvs
|
||||
persistent_notification.dismiss(self.hass, f'{DOMAIN}-login')
|
||||
return user_input
|
||||
|
||||
async def get_cloud_filter_schema(self, user_input, errors, schema=None, via_did=False, home_ids=None):
|
||||
if not schema:
|
||||
schema = vol.Schema({})
|
||||
dvs = self.devices or []
|
||||
if not dvs:
|
||||
errors['base'] = 'none_devices'
|
||||
else:
|
||||
grp = {}
|
||||
vls = {}
|
||||
homes = {}
|
||||
fls = ['did'] if via_did else ['model', 'home_id', 'ssid', 'bssid']
|
||||
for f in fls:
|
||||
for d in dvs:
|
||||
v = d.get(f)
|
||||
if v is None:
|
||||
continue
|
||||
if home_id := d.get('home_id', 0):
|
||||
homes.setdefault(home_id, d.get('home_name') or 'Default Home')
|
||||
if f in ['did'] and v in user_input.get(f'{f}_list', []):
|
||||
pass
|
||||
elif home_ids and home_id not in home_ids:
|
||||
continue
|
||||
grp.setdefault(v, 0)
|
||||
grp[v] += 1
|
||||
vls.setdefault(f, {})
|
||||
dnm = f'{d.get("name")}'
|
||||
des = '<empty>' if v == '' else v
|
||||
if f == 'home_id':
|
||||
des = d.get('home_name') or des
|
||||
if f in ['did']:
|
||||
if MiotCloud.is_hide(d):
|
||||
continue
|
||||
dip = d.get('localip')
|
||||
if not dip or d.get('pid') not in [0, '0', '8', '', None]:
|
||||
dip = d.get('model')
|
||||
vls[f][v] = f'{dnm} ({dip})'
|
||||
elif f in ['model']:
|
||||
if grp[v] > 1:
|
||||
dnm += f' * {grp[v]}'
|
||||
vls[f][v] = f'{des} ({dnm})'
|
||||
else:
|
||||
vls[f][v] = f'{des} ({grp[v]})'
|
||||
ies = {
|
||||
'exclude': 'Exclude (排除)',
|
||||
'include': 'Include (包含)',
|
||||
}
|
||||
for f in fls:
|
||||
if not vls.get(f):
|
||||
continue
|
||||
fk = f'filter_{f}'
|
||||
fl = f'{f}_list'
|
||||
lst = vls.get(f, {})
|
||||
lst = dict(sorted(lst.items()))
|
||||
ols = [
|
||||
v
|
||||
for v in user_input.get(fl, [])
|
||||
if v in lst
|
||||
]
|
||||
schema = schema.extend({
|
||||
vol.Required(fk, default=user_input.get(fk, 'exclude')): vol.In(ies),
|
||||
vol.Optional(fl, default=ols): cv.multi_select(lst),
|
||||
})
|
||||
if via_did and homes:
|
||||
schema = schema.extend({
|
||||
vol.Optional('home_ids', default=[]): cv.multi_select(homes),
|
||||
})
|
||||
tip = ''
|
||||
if user_input.get(CONF_CONN_MODE) == 'local':
|
||||
url = 'https://github.com/al-one/hass-xiaomi-miot/issues/100#issuecomment-855183156'
|
||||
if user_input.get(CONF_SERVER_COUNTRY) == 'cn':
|
||||
tip = '⚠️ 在本地模式下,所有包含的设备都将通过本地miot协议连接,如果包含了不支持本地miot协议的设备,其实体会不可用,' \
|
||||
f'建议只选择[支持本地模式的设备]({url})。'
|
||||
else:
|
||||
tip = '⚠️ In the local mode, all included devices will be connected via the local miot protocol.' \
|
||||
'If the devices that does not support the local miot protocol are included,' \
|
||||
'they will be unavailable. It is recommended to include only ' \
|
||||
f'[the devices that supports the local mode]({url}).'
|
||||
self.placeholders['tip'] = tip
|
||||
return schema
|
||||
|
||||
|
||||
class XiaomiMiotFlowHandler(config_entries.ConfigFlow, BaseFlowHandler, domain=DOMAIN):
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
filter_models = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(entry: config_entries.ConfigEntry):
|
||||
return OptionsFlowHandler(entry)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
self.context['last_step'] = False
|
||||
init_integration_data(self.hass)
|
||||
errors = {}
|
||||
if user_input is None:
|
||||
user_input = {}
|
||||
else:
|
||||
action = user_input.get('action')
|
||||
if action in ['account', 'cloud']:
|
||||
return await self.async_step_cloud()
|
||||
elif action in ['customizing_entity', 'customizing_device']:
|
||||
self.context['customizing_via'] = action
|
||||
return await self.async_step_customizing()
|
||||
else:
|
||||
return await self.async_step_token()
|
||||
prev_action = user_input.get('action', 'account')
|
||||
if prev_action == 'cloud':
|
||||
prev_action = 'account'
|
||||
actions = {
|
||||
'account': 'Add devices using Mi Account (账号集成)',
|
||||
'token': 'Add device using host/token (局域网集成)',
|
||||
}
|
||||
if self.hass.data[DOMAIN].get('entities', {}):
|
||||
actions.update({
|
||||
'customizing_device': 'Customizing device (自定义设备) <推荐>',
|
||||
'customizing_entity': 'Customizing entity (自定义实体)',
|
||||
})
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required('action', default=prev_action): vol.In(actions),
|
||||
}),
|
||||
errors=errors,
|
||||
last_step=self.context.get('last_step'),
|
||||
)
|
||||
|
||||
async def async_step_token(self, user_input=None):
|
||||
errors = {}
|
||||
if user_input is None:
|
||||
user_input = {}
|
||||
else:
|
||||
await check_miio_device(self.hass, user_input, errors)
|
||||
if user_input.get('unique_did'):
|
||||
await self.async_set_unique_id(user_input['unique_did'])
|
||||
self._abort_if_unique_id_configured()
|
||||
if user_input.get('miio_info'):
|
||||
user_input[CONF_CONFIG_VERSION] = ENTRY_VERSION
|
||||
return self.async_create_entry(
|
||||
title=user_input.get(CONF_NAME),
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id='token',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, vol.UNDEFINED)): str,
|
||||
vol.Required(CONF_TOKEN, default=user_input.get(CONF_TOKEN, vol.UNDEFINED)):
|
||||
vol.All(str, vol.Length(min=32, max=32)),
|
||||
vol.Optional(CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)): str,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=user_input.get(CONF_SCAN_INTERVAL, DEFAULT_INTERVAL)):
|
||||
cv.positive_int,
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_cloud(self, user_input=None):
|
||||
# pylint: disable=invalid-name
|
||||
self.CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
errors = {}
|
||||
if user_input is None:
|
||||
user_input = {}
|
||||
else:
|
||||
await self.check_xiaomi_account(user_input, errors, renew_devices=True)
|
||||
if not errors:
|
||||
user_input['filtering'] = True
|
||||
self.config_data = user_input
|
||||
self.filter_models = user_input.get('filter_models')
|
||||
return await self.async_step_cloud_filter(user_input)
|
||||
schema = {}
|
||||
if self.context.get('need_verify'):
|
||||
schema.update({
|
||||
vol.Optional('verify_ticket', default=''): str,
|
||||
})
|
||||
if self.context.get('captchaIck'):
|
||||
schema.update({
|
||||
vol.Optional('captcha', default=''): str,
|
||||
})
|
||||
schema.update({
|
||||
vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME, vol.UNDEFINED)): str,
|
||||
vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, vol.UNDEFINED)): str,
|
||||
vol.Required(CONF_SERVER_COUNTRY, default=user_input.get(CONF_SERVER_COUNTRY, 'cn')):
|
||||
vol.In(CLOUD_SERVERS),
|
||||
vol.Required(CONF_CONN_MODE, default=user_input.get(CONF_CONN_MODE, 'auto')):
|
||||
vol.In(CONN_MODES),
|
||||
vol.Optional('trans_options', default=user_input.get('trans_options', False)): bool,
|
||||
vol.Optional('filter_models', default=user_input.get('filter_models', False)): bool,
|
||||
})
|
||||
return self.async_show_form(
|
||||
step_id='cloud',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
description_placeholders=self.pop_placeholders(),
|
||||
)
|
||||
|
||||
async def async_step_cloud_filter(self, user_input=None):
|
||||
errors = {}
|
||||
schema = vol.Schema({})
|
||||
if user_input is None:
|
||||
user_input = {}
|
||||
via_did = not self.filter_models
|
||||
home_ids = user_input.pop('home_ids', [])
|
||||
if user_input.pop('filtering', None) or home_ids:
|
||||
schema = await self.get_cloud_filter_schema(user_input, errors, schema, via_did=via_did, home_ids=home_ids)
|
||||
elif user_input:
|
||||
if not self.config_data:
|
||||
self.config_data = {}
|
||||
self.config_data.update({
|
||||
**(self.cloud.to_config() or {}),
|
||||
**user_input,
|
||||
'filter_models': self.filter_models,
|
||||
CONF_CONFIG_VERSION: ENTRY_VERSION,
|
||||
})
|
||||
_LOGGER.debug('Setup xiaomi cloud: %s', {**self.config_data, CONF_PASSWORD: '*', 'service_token': '*'})
|
||||
return self.async_create_entry(
|
||||
title=f"Xiaomi: {self.config_data.get('user_id')}",
|
||||
data=self.config_data,
|
||||
)
|
||||
else:
|
||||
errors['base'] = 'unknown'
|
||||
return self.async_show_form(
|
||||
step_id='cloud_filter',
|
||||
data_schema=schema,
|
||||
errors=errors,
|
||||
description_placeholders=self.pop_placeholders(),
|
||||
)
|
||||
|
||||
async def async_step_customizing(self, user_input=None):
|
||||
tip = ''
|
||||
via = self.context.get('customizing_via') or 'customizing_device'
|
||||
self.context['customizing_via'] = via
|
||||
entry = await self.async_set_unique_id(f'{DOMAIN}-customizes')
|
||||
entry_data = copy.deepcopy(dict(entry.data) if entry else {})
|
||||
customizes = {}
|
||||
errors = {}
|
||||
schema = {}
|
||||
user_input = user_input or {}
|
||||
bool2selects = [
|
||||
'auto_cloud',
|
||||
'ignore_offline',
|
||||
'miot_local',
|
||||
'miot_cloud',
|
||||
'miot_cloud_write',
|
||||
'miot_cloud_action',
|
||||
'check_lan',
|
||||
'unreadable_properties',
|
||||
]
|
||||
main_options = {
|
||||
'bool2selects': cv.multi_select({}),
|
||||
'interval_seconds': cv.string,
|
||||
'chunk_properties': cv.string,
|
||||
'sensor_properties': cv.string,
|
||||
'binary_sensor_properties': cv.string,
|
||||
'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_actions': cv.string,
|
||||
'select_actions': cv.string,
|
||||
'text_actions': cv.string,
|
||||
'exclude_miot_services': cv.string,
|
||||
'exclude_miot_properties': cv.string,
|
||||
'configuration_entities': cv.string,
|
||||
'diagnostic_entities': cv.string,
|
||||
'cloud_delay_update': cv.string,
|
||||
}
|
||||
options = {
|
||||
'entity_category': cv.string,
|
||||
}
|
||||
|
||||
last_step = self.context.pop('last_step', False)
|
||||
customize_key = self.context.pop('customize_key', None)
|
||||
if last_step and customize_key:
|
||||
reset = user_input.pop('reset_customizes', None)
|
||||
b2s = user_input.pop('bool2selects', None) or []
|
||||
customize_data = entry_data.setdefault(via, {})
|
||||
if reset:
|
||||
entry_data[via].pop(customize_key, None)
|
||||
elif self.context.get('yaml_mode'):
|
||||
yml = user_input.get('yaml_customizes') or {}
|
||||
if yml:
|
||||
customize_data[customize_key] = yml
|
||||
else:
|
||||
entry_data[via].pop(customize_key, None)
|
||||
else:
|
||||
for k in b2s:
|
||||
user_input[k] = True
|
||||
customize_data[customize_key] = {
|
||||
k: v
|
||||
for k, v in user_input.items()
|
||||
if v not in [' ', '', None, vol.UNDEFINED]
|
||||
}
|
||||
if entry:
|
||||
self.hass.config_entries.async_update_entry(entry, data=entry_data)
|
||||
await self.hass.config_entries.async_reload(entry.entry_id)
|
||||
tip = f'```yaml\n{yaml.dump(entry_data)}\n```'
|
||||
return self.async_abort(
|
||||
reason='config_saved',
|
||||
description_placeholders={'tip': tip},
|
||||
)
|
||||
return self.async_create_entry(title='Xiaomi: Customizes', data=entry_data)
|
||||
|
||||
elif via == 'customizing_entity':
|
||||
if entity := user_input.get('entity'):
|
||||
customizes = entry_data.get(via, {}).get(entity) or {}
|
||||
ent = self.hass.data[DOMAIN].get('entities', {}).get(entity)
|
||||
model = ent.model or ''
|
||||
for k, v in (get_customize_via_entity(ent) or {}).items():
|
||||
customizes.setdefault(k, v)
|
||||
state = self.hass.states.get(entity)
|
||||
tip = f'{state.name}\n{entity}'
|
||||
if model:
|
||||
tip += f'\n[{model}](https://home.miot-spec.com/spec/{model})'
|
||||
if not hasattr(ent, 'parent_entity'):
|
||||
options = {**main_options, **options}
|
||||
get_customize_options(self.hass, options, bool2selects, entity_id=entity, model=model)
|
||||
if options:
|
||||
self.context['last_step'] = True
|
||||
self.context['customize_key'] = entity
|
||||
elif domain := user_input.get('domain'):
|
||||
entities = {}
|
||||
for state in sorted(
|
||||
self.hass.states.async_all(domain),
|
||||
key=lambda item: item.entity_id,
|
||||
):
|
||||
entity = state.entity_id
|
||||
ent = self.hass.data[DOMAIN].get('entities', {}).get(entity)
|
||||
if not ent:
|
||||
continue
|
||||
if user_input.get('only_main_entity') and hasattr(ent, 'parent_entity'):
|
||||
continue
|
||||
entities[entity] = f'{state.name} ({entity})'
|
||||
if entities:
|
||||
schema.update({
|
||||
vol.Required('entity'): vol.In(entities),
|
||||
vol.Optional('yaml_mode', default=user_input.get('yaml_mode', False)): cv.boolean,
|
||||
})
|
||||
else:
|
||||
tip = f'None entities in `{domain}`'
|
||||
else:
|
||||
tip = ('⚠️ 自定义实体后续可能会弃用,推荐通过设备型号**自定义设备**。\n'
|
||||
'The Customization of entity may be deprecated in the future, '
|
||||
'it is recommended to customize the device through the device model.')
|
||||
schema.update({
|
||||
vol.Required('domain', default=user_input.get('domain', vol.UNDEFINED)): vol.In(SUPPORTED_DOMAINS),
|
||||
vol.Optional('only_main_entity', default=user_input.get('only_main_entity', True)): cv.boolean,
|
||||
})
|
||||
|
||||
elif via == 'customizing_device':
|
||||
model = user_input.get('model_specified') or user_input.get('model')
|
||||
if model:
|
||||
customizes = entry_data.get(via, {}).get(model) or {}
|
||||
for k, v in (get_customize_via_model(model) or {}).items():
|
||||
customizes.setdefault(k, v)
|
||||
if '*' in model or ':' in model:
|
||||
tip = model
|
||||
else:
|
||||
tip = f'[{model}](https://home.miot-spec.com/spec/{model})'
|
||||
if ':' not in model:
|
||||
options = {**main_options, **options}
|
||||
get_customize_options(self.hass, options, bool2selects, model=model)
|
||||
if options:
|
||||
self.context['last_step'] = True
|
||||
self.context['customize_key'] = model
|
||||
else:
|
||||
models = {}
|
||||
uds = {}
|
||||
for v in self.hass.data[DOMAIN].values():
|
||||
if isinstance(v, dict):
|
||||
if mod := v.get('miio_info', {}).get(CONF_MODEL):
|
||||
models[mod] = v
|
||||
v = v.get(CONF_XIAOMI_CLOUD)
|
||||
if isinstance(v, MiotCloud):
|
||||
mic = v
|
||||
if mic.user_id not in uds:
|
||||
uds[mic.user_id] = await mic.async_get_devices_by_key('model') or {}
|
||||
models.update(uds[mic.user_id])
|
||||
if models:
|
||||
models = sorted(models.keys())
|
||||
schema.update({
|
||||
vol.Required('model'): vol.In(models),
|
||||
})
|
||||
schema.update({
|
||||
vol.Optional('model_specified'): str,
|
||||
vol.Optional('yaml_mode', default=user_input.get('yaml_mode', False)): cv.boolean,
|
||||
})
|
||||
|
||||
if last_step := self.context.get('last_step', last_step):
|
||||
doc = 'https://github.com/al-one/hass-xiaomi-miot/issues/600'
|
||||
if in_china(self.hass):
|
||||
tip = f'[📚 自定义选项说明文档]({doc})\n\n------\n{tip}'
|
||||
else:
|
||||
tip = f'[❓ Need Help]({doc})\n\n------\n{tip}'
|
||||
if not options:
|
||||
tip += f'\n\n无可用的自定义选项。' if in_china(self.hass) else f'\n\nNo customizable options are available.'
|
||||
|
||||
customizes.pop('extend_miot_specs', None)
|
||||
self.context['yaml_mode'] = user_input.get('yaml_mode')
|
||||
if self.context['yaml_mode']:
|
||||
schema.update({
|
||||
vol.Optional('yaml_customizes', default=customizes): ObjectSelector(),
|
||||
})
|
||||
else:
|
||||
if 'bool2selects' in options:
|
||||
options['bool2selects'] = cv.multi_select(dict(zip(bool2selects, bool2selects)))
|
||||
customizes['bool2selects'] = [
|
||||
k
|
||||
for k in bool2selects
|
||||
if customizes.get(k)
|
||||
]
|
||||
schema.update({
|
||||
vol.Optional(k, default=customizes.get(k, vol.UNDEFINED), description=k): v
|
||||
for k, v in options.items()
|
||||
})
|
||||
schema.update({
|
||||
vol.Optional('reset_customizes', default=False): cv.boolean,
|
||||
})
|
||||
if customizes:
|
||||
customizes.pop('bool2selects', None)
|
||||
tip += f'\n```yaml\n{yaml.dump(customizes)}\n```'
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='customizing',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
description_placeholders={'tip': tip},
|
||||
last_step=last_step,
|
||||
)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow, BaseFlowHandler):
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||
if HA_VERSION < '2024.12':
|
||||
self.config_entry = config_entry
|
||||
|
||||
@property
|
||||
def saved_config(self):
|
||||
return {
|
||||
**self.config_entry.data,
|
||||
**self.config_entry.options,
|
||||
}
|
||||
|
||||
@property
|
||||
def filter_models(self):
|
||||
data = self.saved_config
|
||||
if data.get('did_list'):
|
||||
return False
|
||||
if data.get('model_list'):
|
||||
return True
|
||||
if 'did_list' in data:
|
||||
return False
|
||||
if 'model_list' in data:
|
||||
return True
|
||||
return data.get('filter_models', False)
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
data = self.config_entry.data
|
||||
if CONF_USERNAME in data:
|
||||
self.config_data = self.saved_config
|
||||
return await self.async_step_cloud()
|
||||
|
||||
if 'customizing_entity' in data or 'customizing_device' in data:
|
||||
tip = f'```yaml\n{yaml.dump(dict(data))}\n```'
|
||||
return self.async_abort(
|
||||
reason='show_customizes',
|
||||
description_placeholders={'tip': tip},
|
||||
)
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
errors = {}
|
||||
if isinstance(user_input, dict):
|
||||
cfg = {}
|
||||
opt = {}
|
||||
for k, v in user_input.items():
|
||||
if k in [CONF_HOST, CONF_TOKEN, CONF_NAME, CONF_SCAN_INTERVAL]:
|
||||
cfg[k] = v
|
||||
else:
|
||||
opt[k] = v
|
||||
await check_miio_device(self.hass, user_input, errors)
|
||||
if user_input.get('miio_info'):
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data={**self.config_entry.data, **cfg}
|
||||
)
|
||||
return self.async_create_entry(title='', data=opt)
|
||||
else:
|
||||
user_input = self.saved_config
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, vol.UNDEFINED)): str,
|
||||
vol.Required(CONF_TOKEN, default=user_input.get(CONF_TOKEN, vol.UNDEFINED)):
|
||||
vol.All(str, vol.Length(min=32, max=32)),
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=user_input.get(CONF_SCAN_INTERVAL, DEFAULT_INTERVAL)):
|
||||
cv.positive_int,
|
||||
vol.Optional('miot_cloud', default=user_input.get('miot_cloud', False)): bool,
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_cloud(self, user_input=None):
|
||||
errors = {}
|
||||
prev_input = self.saved_config
|
||||
if isinstance(user_input, dict):
|
||||
user_input = {
|
||||
**prev_input,
|
||||
**user_input,
|
||||
}
|
||||
renew = not not user_input.pop('renew_devices', False)
|
||||
await self.check_xiaomi_account(user_input, errors, renew_devices=renew)
|
||||
if not errors:
|
||||
user_input['filtering'] = True
|
||||
self.config_data.update(user_input)
|
||||
return await self.async_step_cloud_filter(user_input)
|
||||
else:
|
||||
user_input = prev_input
|
||||
schema = {}
|
||||
if self.context.get('need_verify'):
|
||||
schema.update({
|
||||
vol.Optional('verify_ticket', default=''): str,
|
||||
})
|
||||
if self.context.get('captchaIck'):
|
||||
schema.update({
|
||||
vol.Optional('captcha', default=''): str,
|
||||
})
|
||||
if user_input.get('trans_options') == None:
|
||||
user_input['trans_options'] = False
|
||||
schema.update({
|
||||
vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME, vol.UNDEFINED)): str,
|
||||
vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, vol.UNDEFINED)): str,
|
||||
vol.Required(CONF_SERVER_COUNTRY, default=user_input.get(CONF_SERVER_COUNTRY, 'cn')):
|
||||
vol.In(CLOUD_SERVERS),
|
||||
vol.Required(CONF_CONN_MODE, default=user_input.get(CONF_CONN_MODE, DEFAULT_CONN_MODE)):
|
||||
vol.In(CONN_MODES),
|
||||
vol.Optional('renew_devices', default=user_input.get('renew_devices', False)): bool,
|
||||
vol.Optional('trans_options', default=user_input.get('trans_options', False)): bool,
|
||||
vol.Optional('disable_message', default=user_input.get('disable_message', False)): bool,
|
||||
vol.Optional('disable_scene_history', default=user_input.get('disable_scene_history', False)): bool,
|
||||
})
|
||||
return self.async_show_form(
|
||||
step_id='cloud',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
description_placeholders=self.pop_placeholders(),
|
||||
)
|
||||
|
||||
async def async_step_cloud_filter(self, user_input=None):
|
||||
errors = {}
|
||||
schema = vol.Schema({})
|
||||
if user_input is None:
|
||||
user_input = {}
|
||||
via_did = not self.filter_models
|
||||
home_ids = user_input.pop('home_ids', [])
|
||||
if user_input.pop('filtering', None) or home_ids:
|
||||
user_input = {
|
||||
**self.saved_config,
|
||||
**user_input,
|
||||
}
|
||||
schema = await self.get_cloud_filter_schema(user_input, errors, schema, via_did=via_did, home_ids=home_ids)
|
||||
elif user_input:
|
||||
self.config_data.update({
|
||||
**(self.cloud.to_config() or {}),
|
||||
'filter_models': self.filter_models,
|
||||
**user_input,
|
||||
})
|
||||
self.config_data.pop('filtering', None)
|
||||
self.config_data.pop('verify_ticket', None)
|
||||
if self.filter_models:
|
||||
self.config_data.pop('filter_did', None)
|
||||
self.config_data.pop('did_list', None)
|
||||
else:
|
||||
self.config_data.pop('filter_model', None)
|
||||
self.config_data.pop('model_list', None)
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=self.config_data)
|
||||
_LOGGER.debug('Setup xiaomi cloud: %s', {**self.config_data, CONF_PASSWORD: '*', 'service_token': '*'})
|
||||
return self.async_create_entry(title='', data={})
|
||||
else:
|
||||
errors['base'] = 'unknown'
|
||||
return self.async_show_form(
|
||||
step_id='cloud_filter',
|
||||
data_schema=schema,
|
||||
errors=errors,
|
||||
description_placeholders=self.pop_placeholders(),
|
||||
)
|
||||
|
||||
|
||||
def get_customize_options(hass, options={}, bool2selects=[], entity_id='', model=''): # noqa
|
||||
entity = None
|
||||
domain = ''
|
||||
if entity_id:
|
||||
entity = hass.data[DOMAIN].get('entities', {}).get(entity_id)
|
||||
domain, _ = split_entity_id(entity_id)
|
||||
attrs = entity.extra_state_attributes if entity else {}
|
||||
entity_class = attrs.get('entity_class')
|
||||
|
||||
if domain == 'sensor':
|
||||
if entity_class in ['MiotSensorEntity']:
|
||||
options.update({
|
||||
'state_property': cv.string,
|
||||
})
|
||||
options.update({
|
||||
'value_ratio': cv.string,
|
||||
'state_class': cv.string,
|
||||
'device_class': cv.string,
|
||||
'unit_of_measurement': cv.string,
|
||||
})
|
||||
if entity_class in ['MihomeMessageSensor']:
|
||||
options.update({
|
||||
'filter_home': cv.string,
|
||||
'exclude_type': cv.string,
|
||||
})
|
||||
if entity_class in ['XiaoaiConversationSensor']:
|
||||
options.update({
|
||||
'interval_seconds': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'binary_sensor' or re.search(r'motion|magnet', model, re.I):
|
||||
bool2selects.extend(['reverse_state'])
|
||||
options.update({
|
||||
'state_property': cv.string,
|
||||
'motion_timeout': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'switch' or re.search(r'plug', model, re.I):
|
||||
bool2selects.extend(['reverse_state'])
|
||||
options.update({
|
||||
'descriptions_for_on': cv.string,
|
||||
'descriptions_for_off': cv.string,
|
||||
'stat_power_cost_key': cv.string,
|
||||
'stat_power_cost_type': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'light' or re.search(r'light', model, re.I):
|
||||
bool2selects.extend(['color_temp_reverse'])
|
||||
options.update({
|
||||
'brightness_for_on': cv.string,
|
||||
'brightness_for_off': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'fan' or re.search(r'\.fan\.|airpurifier|airfresh', model, re.I):
|
||||
options.update({
|
||||
'disable_preset_modes': cv.string,
|
||||
'speed_property': cv.string,
|
||||
'percentage_property': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'camera' or re.search(r'camera|videodoll', model, re.I):
|
||||
bool2selects.extend([
|
||||
'keep_streaming', 'use_rtsp_stream', 'use_alarm_playlist',
|
||||
'use_motion_stream', 'sub_motion_stream',
|
||||
])
|
||||
options.update({
|
||||
'video_attribute': cv.string,
|
||||
'motion_stream_slice': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'climate' or re.search(r'aircondition|acpartner|airrtc', model, re.I):
|
||||
bool2selects.extend(['ignore_fan_switch', 'target2current_temp'])
|
||||
options.update({
|
||||
'bind_sensor': cv.string,
|
||||
'turn_on_hvac': cv.string,
|
||||
'current_temp_property': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'cover' or re.search(r'airer|curtain|wopener', model, re.I):
|
||||
bool2selects.extend([
|
||||
'motor_reverse', 'auto_position_reverse', 'position_reverse', 'target2current_position',
|
||||
])
|
||||
options.update({
|
||||
'closed_position': cv.string,
|
||||
'deviated_position': cv.string,
|
||||
'open_texts': cv.string,
|
||||
'close_texts': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'media_player' or re.search(r'\.tv\.|tvbox|projector', model, re.I):
|
||||
bool2selects.extend(['turn_off_screen', 'xiaoai_silent'])
|
||||
options.update({
|
||||
'miot_did': cv.string,
|
||||
'bind_xiaoai': cv.string,
|
||||
'sources_via_apps': cv.string,
|
||||
'sources_via_keycodes': cv.string,
|
||||
'screenshot_compress': cv.string,
|
||||
'television_name': cv.string,
|
||||
'mitv_lan_host': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'number':
|
||||
bool2selects.extend(['restore_state'])
|
||||
|
||||
if domain == 'humidifier':
|
||||
options.update({
|
||||
'mode_property': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'device_tracker' or re.search(r'watch', model, re.I):
|
||||
options.update({
|
||||
'coord_type': cv.string,
|
||||
})
|
||||
bool2selects.extend(['disable_location_name'])
|
||||
|
||||
if re.search(r'sensor_occupy', model, re.I):
|
||||
options.update({
|
||||
'scanner_properties': cv.string,
|
||||
})
|
||||
|
||||
if domain == 'text' and re.search(r'execute_text_directive', entity_id, re.I):
|
||||
bool2selects.extend(['silent_execution'])
|
||||
|
||||
if 'yeelink.' in model:
|
||||
options.update({
|
||||
'yeelight_smooth_on': cv.string,
|
||||
'yeelight_smooth_off': cv.string,
|
||||
})
|
||||
|
||||
return options
|
||||
@@ -0,0 +1,11 @@
|
||||
from .device import Device, DeviceInfo
|
||||
from .hass_entry import HassEntry
|
||||
from .hass_entity import BasicEntity, XEntity
|
||||
|
||||
__all__ = [
|
||||
'Device',
|
||||
'DeviceInfo',
|
||||
'HassEntry',
|
||||
'BasicEntity',
|
||||
'XEntity',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,114 @@
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
from homeassistant.const import __version__ as HAVERSION # noqa
|
||||
from awesomeversion import AwesomeVersion
|
||||
from .device_customizes import DEVICE_CUSTOMIZES, GLOBAL_CONVERTERS # noqa
|
||||
from .miot_local_devices import MIOT_LOCAL_MODELS # noqa
|
||||
from .translation_languages import TRANSLATION_LANGUAGES # noqa
|
||||
|
||||
DOMAIN = 'xiaomi_miot'
|
||||
DEFAULT_NAME = 'Xiaomi Miot'
|
||||
HA_VERSION = AwesomeVersion(HAVERSION)
|
||||
|
||||
CONF_MODEL = 'model'
|
||||
CONF_XIAOMI_CLOUD = 'xiaomi_cloud'
|
||||
CONF_SERVER_COUNTRY = 'server_country'
|
||||
CONF_CONN_MODE = 'conn_mode'
|
||||
CONF_CONFIG_VERSION = 'config_version'
|
||||
|
||||
DEFAULT_CONN_MODE = 'cloud'
|
||||
|
||||
SUPPORTED_DOMAINS = [
|
||||
'sensor',
|
||||
'binary_sensor',
|
||||
'switch',
|
||||
'number',
|
||||
'select',
|
||||
'button',
|
||||
'text',
|
||||
'light',
|
||||
'fan',
|
||||
'climate',
|
||||
'cover',
|
||||
'humidifier',
|
||||
'media_player',
|
||||
'camera',
|
||||
'vacuum',
|
||||
'water_heater',
|
||||
'device_tracker',
|
||||
'remote',
|
||||
'alarm_control_panel',
|
||||
]
|
||||
|
||||
CLOUD_SERVERS = {
|
||||
'cn': '中国大陆',
|
||||
'tw': '中國台灣',
|
||||
'de': 'Europe',
|
||||
'i2': 'India',
|
||||
'ru': 'Russia',
|
||||
'sg': 'Singapore',
|
||||
'us': 'United States',
|
||||
}
|
||||
|
||||
try:
|
||||
# python 3.11
|
||||
from enum import StrEnum
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
class StrEnum(str, Enum):
|
||||
pass
|
||||
|
||||
try:
|
||||
# hass 2023.3
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
JsonObjectType = dict
|
||||
|
||||
try:
|
||||
# hass 2023.7
|
||||
from homeassistant.core import ServiceResponse, SupportsResponse
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
SupportsResponse = None
|
||||
ServiceResponse = Union[dict, None]
|
||||
|
||||
try:
|
||||
# hass 2024.10
|
||||
from homeassistant.components.camera import CameraState
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
class CameraState(StrEnum):
|
||||
RECORDING = 'recording'
|
||||
STREAMING = 'streaming'
|
||||
IDLE = 'idle'
|
||||
|
||||
try:
|
||||
# hass 2024.11
|
||||
from homeassistant.core_config import DATA_CUSTOMIZE
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
from homeassistant.helpers.entity import DATA_CUSTOMIZE
|
||||
|
||||
try:
|
||||
# hass 2024.11
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
class AlarmControlPanelState(StrEnum):
|
||||
"""Alarm control panel entity states."""
|
||||
DISARMED = "disarmed"
|
||||
ARMED_HOME = "armed_home"
|
||||
ARMED_AWAY = "armed_away"
|
||||
ARMED_NIGHT = "armed_night"
|
||||
ARMED_VACATION = "armed_vacation"
|
||||
ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
|
||||
PENDING = "pending"
|
||||
ARMING = "arming"
|
||||
DISARMING = "disarming"
|
||||
TRIGGERED = "triggered"
|
||||
|
||||
try:
|
||||
# hass 2025.1
|
||||
from homeassistant.components.vacuum import VacuumActivity
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
class VacuumActivity(StrEnum):
|
||||
CLEANING = "cleaning"
|
||||
DOCKED = "docked"
|
||||
RETURNING = "returning"
|
||||
ERROR = "error"
|
||||
@@ -0,0 +1,349 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from dataclasses import dataclass
|
||||
from homeassistant.util import color, percentage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device
|
||||
from .miot_spec import MiotService, MiotProperty, MiotAction
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseConv:
|
||||
attr: str
|
||||
domain: str = None
|
||||
mi: str | int = None
|
||||
attrs: list = None
|
||||
option: dict = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.attrs is None:
|
||||
self.attrs = []
|
||||
if self.option is None:
|
||||
self.option = {}
|
||||
|
||||
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.full_name] = value
|
||||
|
||||
# from hass
|
||||
def encode(self, device: 'Device', payload: dict, value):
|
||||
params = None
|
||||
if self.mi and 'prop.' in self.mi:
|
||||
_, s, p = self.mi.split('.')
|
||||
payload['method'] = 'set_properties'
|
||||
params = {'siid': int(s), 'piid': int(p)}
|
||||
if params:
|
||||
params.update({'did': device.did, 'value': value})
|
||||
payload.setdefault('params', []).append(params)
|
||||
|
||||
@dataclass
|
||||
class InfoConv(BaseConv):
|
||||
attr: str = 'info'
|
||||
domain: str = 'button'
|
||||
|
||||
def decode(self, device: 'Device', payload: dict, value):
|
||||
updater = device.data.get('updater')
|
||||
infos = {
|
||||
self.full_name: device.name,
|
||||
'model': device.model,
|
||||
'did': device.info.did,
|
||||
'mac': device.info.mac,
|
||||
'lan_ip': device.info.host,
|
||||
'app_link': device.app_link,
|
||||
'miot_type': device.info.urn,
|
||||
'available': device.available,
|
||||
'home_room': device.info.home_room,
|
||||
'icon': self.option.get('icon') if device.available else 'mdi:information-off',
|
||||
'updater': updater or 'none',
|
||||
}
|
||||
customizes = {**device.customizes}
|
||||
customizes.pop('append_converters', None)
|
||||
customizes.pop('extend_miot_specs', None)
|
||||
payload.update({
|
||||
**infos,
|
||||
**device.props,
|
||||
'converters': [c.full_name for c in device.converters],
|
||||
'customizes': customizes,
|
||||
**infos,
|
||||
})
|
||||
if device.available:
|
||||
payload.pop('miot_error', None)
|
||||
if device.miot_results:
|
||||
if err := device.miot_results.errors:
|
||||
payload['miot_error'] = str(err)
|
||||
|
||||
def encode(self, device: 'Device', payload: dict, value):
|
||||
payload.update({
|
||||
'method': 'update_status',
|
||||
})
|
||||
|
||||
@dataclass
|
||||
class AttrConv(BaseConv):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class MiotPropConv(BaseConv):
|
||||
prop: 'MiotProperty' = None
|
||||
desc: bool = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.prop:
|
||||
if not self.mi:
|
||||
from .miot_spec import MiotSpec
|
||||
self.mi = MiotSpec.unique_prop(self.prop.siid, piid=self.prop.iid)
|
||||
if self.desc == None:
|
||||
self.desc = self.prop.use_desc(self.domain)
|
||||
|
||||
def decode(self, device: 'Device', payload: dict, value):
|
||||
if self.desc and self.prop:
|
||||
value = self.prop.list_description(value)
|
||||
if self.domain == 'sensor' and isinstance(value, str):
|
||||
value = value.lower()
|
||||
super().decode(device, payload, value)
|
||||
|
||||
def encode(self, device: 'Device', payload: dict, value):
|
||||
if self.prop:
|
||||
if self.desc:
|
||||
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)
|
||||
|
||||
@dataclass
|
||||
class MiotPropValueConv(MiotPropConv):
|
||||
value: Any = None
|
||||
description: str = None
|
||||
|
||||
def decode(self, device: 'Device', payload: dict, value):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class MiotActionConv(BaseConv):
|
||||
action: 'MiotAction' = None
|
||||
prop: 'MiotProperty' = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if not self.mi:
|
||||
from .miot_spec import MiotSpec
|
||||
self.mi = MiotSpec.unique_prop(self.action.siid, aiid=self.action.iid)
|
||||
if not self.prop:
|
||||
self.prop = self.action.in_properties()[0] if self.action.ins else None
|
||||
|
||||
def decode(self, device: 'Device', payload: dict, value):
|
||||
super().decode(device, payload, value)
|
||||
|
||||
def encode(self, device: 'Device', payload: dict, value):
|
||||
if self.prop and isinstance(value, str):
|
||||
if self.prop.value_list or self.prop.value_range:
|
||||
value = self.prop.list_value(value)
|
||||
elif self.prop.is_integer:
|
||||
value = int(value)
|
||||
ins = value if isinstance(value, list) else [] if value is None else [value]
|
||||
_, s, p = self.mi.split('.')
|
||||
payload['method'] = 'action'
|
||||
payload['param'] = {
|
||||
'did': device.did,
|
||||
'siid': int(s),
|
||||
'aiid': int(p),
|
||||
'in': ins,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class MiotServiceConv(MiotPropConv):
|
||||
attr: str = None
|
||||
service: 'MiotService' = None
|
||||
prop: 'MiotProperty' = None
|
||||
main_props: list = None
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.prop and self.service and self.main_props:
|
||||
self.prop = self.service.get_property(*self.main_props)
|
||||
super().__post_init__()
|
||||
if self.attr:
|
||||
pass
|
||||
elif self.prop:
|
||||
self.attr = self.prop.full_name
|
||||
elif self.service:
|
||||
self.attr = self.service.desc_name
|
||||
|
||||
@dataclass
|
||||
class MiotSensorConv(MiotServiceConv):
|
||||
domain: str = 'sensor'
|
||||
|
||||
@dataclass
|
||||
class MiotSwitchConv(MiotServiceConv):
|
||||
domain: str = 'switch'
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.main_props:
|
||||
self.main_props = ['on', 'switch']
|
||||
super().__post_init__()
|
||||
|
||||
@dataclass
|
||||
class MiotLightConv(MiotSwitchConv):
|
||||
domain: str = 'light'
|
||||
|
||||
@dataclass
|
||||
class MiotBrightnessConv(MiotPropConv):
|
||||
def decode(self, device: 'Device', payload: dict, value: int):
|
||||
max = self.prop.range_max()
|
||||
if max != None:
|
||||
super().decode(device, payload, value / max * 255.0)
|
||||
|
||||
def encode(self, device: 'Device', payload: dict, value: float):
|
||||
max = self.prop.range_max()
|
||||
if max != None:
|
||||
value = round(value / 255.0 * max)
|
||||
super().encode(device, payload, int(value))
|
||||
|
||||
@dataclass
|
||||
class MiotColorTempConv(MiotPropConv):
|
||||
def decode(self, device: 'Device', payload: dict, value: int):
|
||||
if self.prop.unit == 'percentage':
|
||||
if not value:
|
||||
return
|
||||
value = self.percentage_to_kelvin(value)
|
||||
elif self.prop.unit != 'kelvin':
|
||||
if not value:
|
||||
return
|
||||
value = round(1000000.0 / value)
|
||||
super().decode(device, payload, value)
|
||||
|
||||
def encode(self, device: 'Device', payload: dict, value: int):
|
||||
if self.prop.unit == 'percentage':
|
||||
if not value:
|
||||
return
|
||||
value = self.kelvin_to_percentage(value)
|
||||
elif self.prop.unit != 'kelvin':
|
||||
if not value:
|
||||
return
|
||||
value = round(1000000.0 / value)
|
||||
|
||||
if value < self.prop.range_min():
|
||||
value = self.prop.range_min()
|
||||
if value > self.prop.range_max():
|
||||
value = self.prop.range_max()
|
||||
super().encode(device, payload, value)
|
||||
|
||||
@staticmethod
|
||||
def percentage_to_kelvin(p: int) -> int:
|
||||
return 6500 - p * 40
|
||||
|
||||
@staticmethod
|
||||
def kelvin_to_percentage(k: int) -> int:
|
||||
return round((6500 - k) / 40)
|
||||
|
||||
@dataclass
|
||||
class MiotRgbColorConv(MiotPropConv):
|
||||
def decode(self, device: 'Device', payload: dict, value: int):
|
||||
super().decode(device, payload, MiotRgbColorConv.int_to_rgb(value))
|
||||
|
||||
def encode(self, device: 'Device', payload: dict, rgb: tuple[int, int, int]):
|
||||
super().encode(device, payload, MiotRgbColorConv.rgb_to_int(rgb))
|
||||
|
||||
@staticmethod
|
||||
def rgb_to_int(rgb: tuple[int, int, int]):
|
||||
num = int(rgb[0]) << 16 | int(rgb[1]) << 8 | int(rgb[2])
|
||||
return int(num)
|
||||
|
||||
@staticmethod
|
||||
def int_to_rgb(value: int):
|
||||
x = int(value)
|
||||
r = (x >> 16) & 0xFF
|
||||
g = (x >> 8) & 0xFF
|
||||
b = x & 0xFF
|
||||
return r, g, b
|
||||
|
||||
@dataclass
|
||||
class MiotHsColorConv(MiotPropConv):
|
||||
def decode(self, device: 'Device', payload: dict, value: int):
|
||||
rgb = MiotRgbColorConv.int_to_rgb(value)
|
||||
super().decode(device, payload, color.color_RGB_to_hs(*rgb))
|
||||
|
||||
def encode(self, device: 'Device', payload: dict, value: tuple):
|
||||
rgb = color.color_hs_to_RGB(*value)
|
||||
super().encode(device, payload, MiotRgbColorConv.rgb_to_int(rgb))
|
||||
|
||||
@dataclass
|
||||
class MiotFanConv(MiotServiceConv):
|
||||
domain: str = 'fan'
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.main_props:
|
||||
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', 'target_temperature']
|
||||
super().__post_init__()
|
||||
|
||||
@dataclass
|
||||
class MiotCoverConv(MiotServiceConv):
|
||||
domain: str = 'cover'
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.main_props:
|
||||
self.main_props = ['motor_control']
|
||||
super().__post_init__()
|
||||
|
||||
@dataclass
|
||||
class MiotCameraConv(MiotServiceConv):
|
||||
domain: str = 'camera'
|
||||
|
||||
@dataclass
|
||||
class MiotHumidifierConv(MiotServiceConv):
|
||||
domain: str = 'humidifier'
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.main_props:
|
||||
self.main_props = ['on', 'target_humidity']
|
||||
super().__post_init__()
|
||||
|
||||
@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.scale_ranged_value_to_int_range(self.ranged, (0, 100), value))
|
||||
super().decode(device, payload, value)
|
||||
|
||||
def encode(self, device: 'Device', payload: dict, value: int):
|
||||
if self.ranged:
|
||||
value = int(percentage.scale_to_ranged_value((0, 100), self.ranged, value))
|
||||
if value < self.ranged[0]:
|
||||
value = self.ranged[0]
|
||||
if value > self.ranged[1]:
|
||||
value = self.ranged[1]
|
||||
super().encode(device, payload, value)
|
||||
|
||||
class MiotTargetPositionConv(PercentagePropConv):
|
||||
pass
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
https://github.com/wandergis/coordTransform_py/blob/master/coordTransform_utils.py
|
||||
"""
|
||||
import math
|
||||
from math import pi
|
||||
|
||||
a = 6378245.0 # 长半轴
|
||||
ee = 0.00669342162296594323 # 偏心率平方
|
||||
x_pi = pi * 3000.0 / 180.0
|
||||
|
||||
|
||||
def wgs84_to_gcj02(lng, lat):
|
||||
"""
|
||||
WGS84转GCJ02(火星坐标系)
|
||||
:param lng:WGS84坐标系的经度
|
||||
:param lat:WGS84坐标系的纬度
|
||||
:return:火星坐标
|
||||
"""
|
||||
if out_of_china(lng, lat): # 判断是否在国内
|
||||
return [lng, lat]
|
||||
dlat = _transformlat(lng - 105.0, lat - 35.0)
|
||||
dlng = _transformlng(lng - 105.0, lat - 35.0)
|
||||
radlat = lat / 180.0 * pi
|
||||
magic = math.sin(radlat)
|
||||
magic = 1 - ee * magic * magic
|
||||
sqrtmagic = math.sqrt(magic)
|
||||
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
|
||||
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
|
||||
mglat = lat + dlat
|
||||
mglng = lng + dlng
|
||||
return [mglng, mglat]
|
||||
|
||||
|
||||
def wgs84_to_bd09(lon, lat):
|
||||
lon, lat = wgs84_to_gcj02(lon, lat)
|
||||
return gcj02_to_bd09(lon, lat)
|
||||
|
||||
|
||||
def gcj02_to_wgs84(lng, lat):
|
||||
"""
|
||||
GCJ02(火星坐标系)转GPS84
|
||||
:param lng:火星坐标系的经度
|
||||
:param lat:火星坐标系纬度
|
||||
:return:原始坐标
|
||||
"""
|
||||
if out_of_china(lng, lat):
|
||||
return [lng, lat]
|
||||
dlat = _transformlat(lng - 105.0, lat - 35.0)
|
||||
dlng = _transformlng(lng - 105.0, lat - 35.0)
|
||||
radlat = lat / 180.0 * pi
|
||||
magic = math.sin(radlat)
|
||||
magic = 1 - ee * magic * magic
|
||||
sqrtmagic = math.sqrt(magic)
|
||||
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
|
||||
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
|
||||
mglat = lat + dlat
|
||||
mglng = lng + dlng
|
||||
return [lng * 2 - mglng, lat * 2 - mglat]
|
||||
|
||||
|
||||
def gcj02_to_bd09(lng, lat):
|
||||
"""
|
||||
火星坐标系(GCJ-02)转百度坐标系(BD-09)
|
||||
谷歌、高德——>百度
|
||||
:param lng:火星坐标经度
|
||||
:param lat:火星坐标纬度
|
||||
:return:百度坐标
|
||||
"""
|
||||
z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * x_pi)
|
||||
theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * x_pi)
|
||||
bd_lng = z * math.cos(theta) + 0.0065
|
||||
bd_lat = z * math.sin(theta) + 0.006
|
||||
return [bd_lng, bd_lat]
|
||||
|
||||
|
||||
def bd09_to_gcj02(bd_lon, bd_lat):
|
||||
"""
|
||||
百度坐标系(BD-09)转火星坐标系(GCJ-02)
|
||||
百度——>谷歌、高德
|
||||
:param bd_lat:百度坐标纬度
|
||||
:param bd_lon:百度坐标经度
|
||||
:return:转换后的坐标列表形式
|
||||
"""
|
||||
x = bd_lon - 0.0065
|
||||
y = bd_lat - 0.006
|
||||
z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * x_pi)
|
||||
theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi)
|
||||
gg_lng = z * math.cos(theta)
|
||||
gg_lat = z * math.sin(theta)
|
||||
return [gg_lng, gg_lat]
|
||||
|
||||
|
||||
def bd09_to_wgs84(bd_lon, bd_lat):
|
||||
lon, lat = bd09_to_gcj02(bd_lon, bd_lat)
|
||||
return gcj02_to_wgs84(lon, lat)
|
||||
|
||||
|
||||
def _transformlat(lng, lat):
|
||||
ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + \
|
||||
0.1 * lng * lat + 0.2 * math.sqrt(math.fabs(lng))
|
||||
ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 *
|
||||
math.sin(2.0 * lng * pi)) * 2.0 / 3.0
|
||||
ret += (20.0 * math.sin(lat * pi) + 40.0 *
|
||||
math.sin(lat / 3.0 * pi)) * 2.0 / 3.0
|
||||
ret += (160.0 * math.sin(lat / 12.0 * pi) + 320 *
|
||||
math.sin(lat * pi / 30.0)) * 2.0 / 3.0
|
||||
return ret
|
||||
|
||||
|
||||
def _transformlng(lng, lat):
|
||||
ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + \
|
||||
0.1 * lng * lat + 0.1 * math.sqrt(math.fabs(lng))
|
||||
ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 *
|
||||
math.sin(2.0 * lng * pi)) * 2.0 / 3.0
|
||||
ret += (20.0 * math.sin(lng * pi) + 40.0 *
|
||||
math.sin(lng / 3.0 * pi)) * 2.0 / 3.0
|
||||
ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 *
|
||||
math.sin(lng / 30.0 * pi)) * 2.0 / 3.0
|
||||
return ret
|
||||
|
||||
|
||||
def out_of_china(lng, lat):
|
||||
return not (73.66 < lng < 135.05 and 3.86 < lat < 53.55)
|
||||
@@ -0,0 +1,52 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.core import HassJob, HassJobType
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class DataCoordinator(DataUpdateCoordinator):
|
||||
def __init__(self, device: 'Device', update_method, **kwargs):
|
||||
kwargs.setdefault('always_update', True)
|
||||
|
||||
if callable(update_method):
|
||||
name = update_method.__name__
|
||||
elif isinstance(update_method, str):
|
||||
name = update_method
|
||||
update_method = getattr(device, name, None)
|
||||
else:
|
||||
raise ValueError('Invalid update method')
|
||||
name = kwargs.pop('name', name)
|
||||
|
||||
super().__init__(
|
||||
device.hass,
|
||||
logger=device.log,
|
||||
name=f'{device.unique_id}-{name}',
|
||||
update_method=update_method,
|
||||
**kwargs,
|
||||
)
|
||||
self.device = device
|
||||
if not hasattr(self, 'setup_method'):
|
||||
# hass v2024.7-
|
||||
self.async_add_listener(self.coordinator_updated)
|
||||
|
||||
async def async_setup(self, index=0):
|
||||
await self._async_setup()
|
||||
|
||||
job = HassJob(self._async_refresh_later, job_type=HassJobType.Coroutinefunction)
|
||||
async_call_later(self.hass, index, job)
|
||||
|
||||
async def _async_setup(self):
|
||||
"""Set up coordinator."""
|
||||
self.async_add_listener(self.coordinator_updated)
|
||||
|
||||
def coordinator_updated(self):
|
||||
_LOGGER.debug('%s: Coordinator updated: %s', self.device.name_model, [self.name, self.data])
|
||||
|
||||
async def _async_refresh_later(self, _=None):
|
||||
await self.async_request_refresh()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,273 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Optional, Callable
|
||||
from functools import cached_property
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity, EntityCategory
|
||||
from homeassistant.helpers.restore_state import ExtraStoredData, RestoredExtraData
|
||||
|
||||
from .const import DOMAIN
|
||||
from .utils import get_customize_via_entity, wildcard_models, CustomConfigHelper
|
||||
from .miot_spec import MiotService, MiotProperty, MiotAction
|
||||
from .converters import BaseConv, InfoConv, MiotServiceConv, MiotPropConv, MiotActionConv
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BasicEntity(Entity, CustomConfigHelper):
|
||||
hass: HomeAssistant = None
|
||||
device: 'Device' = None
|
||||
conv: 'BaseConv' = None
|
||||
|
||||
def custom_config(self, key=None, default=None):
|
||||
return get_customize_via_entity(self, key, default)
|
||||
|
||||
def global_config(self, key=None, default=None):
|
||||
if not self.hass:
|
||||
return default
|
||||
cfg = self.hass.data[DOMAIN]['config'] or {}
|
||||
return cfg if key is None else cfg.get(key, default)
|
||||
|
||||
def filter_state_attributes(self, dat: dict):
|
||||
if exl := self.global_config('exclude_state_attributes'):
|
||||
if not isinstance(exl, (list, tuple)):
|
||||
exl = [exl]
|
||||
dat = {
|
||||
k: v
|
||||
for k, v in dat.items()
|
||||
if k not in exl
|
||||
}
|
||||
return dat
|
||||
|
||||
@cached_property
|
||||
def extra_state_attributes(self):
|
||||
if hasattr(self, '_attr_extra_state_attributes'):
|
||||
return self.filter_state_attributes(self._attr_extra_state_attributes)
|
||||
return None
|
||||
|
||||
async def async_get_properties(self, mapping, update_entity=False, **kwargs):
|
||||
return await self.device.async_get_properties(mapping, update_entity, **kwargs)
|
||||
|
||||
async def async_set_property(self, field, value):
|
||||
return await self.device.async_set_property(field, value)
|
||||
|
||||
async def async_set_miot_property(self, siid, piid, value, **kwargs):
|
||||
return await self.device.async_set_miot_property(siid, piid, value, **kwargs)
|
||||
|
||||
async def async_call_action(self, siid, aiid=None, params=None, **kwargs):
|
||||
if isinstance(siid, MiotAction):
|
||||
if params is None:
|
||||
params = aiid
|
||||
action = siid
|
||||
siid = action.service.iid
|
||||
aiid = action.iid
|
||||
return await self.device.async_call_action(siid, aiid, params, **kwargs)
|
||||
|
||||
async def async_miio_command(self, method, params=None, **kwargs):
|
||||
if not self.device.local:
|
||||
return {'error': 'Unsupported'}
|
||||
if params is None:
|
||||
params = []
|
||||
_LOGGER.debug('%s: Send miio command: %s(%s)', self.device.name_model, method, params)
|
||||
return await self.device.local.async_send(method, params)
|
||||
|
||||
|
||||
class XEntity(BasicEntity):
|
||||
CLS: dict[str, Callable] = {}
|
||||
|
||||
log = _LOGGER
|
||||
added = False
|
||||
_attr_available = False
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_miot_service: Optional[MiotService] = None
|
||||
_miot_property: Optional[MiotProperty] = None
|
||||
_miot_action: Optional[MiotAction] = None
|
||||
|
||||
def __init__(self, device: 'Device', conv: 'BaseConv'):
|
||||
self.device = device
|
||||
self.hass = device.hass
|
||||
self.conv = conv
|
||||
self.attr = conv.full_name
|
||||
self.log = device.log
|
||||
|
||||
if isinstance(conv, MiotServiceConv):
|
||||
self.entity_id = conv.service.generate_entity_id(self, conv.domain)
|
||||
self._attr_name = str(conv.service.friendly_desc)
|
||||
self._attr_translation_key = conv.service.name
|
||||
self._miot_service = conv.service
|
||||
self._miot_property = conv.prop
|
||||
|
||||
elif isinstance(conv, MiotPropConv):
|
||||
self.entity_id = conv.prop.generate_entity_id(self, conv.domain)
|
||||
self._attr_name = str(conv.prop.friendly_desc)
|
||||
self._attr_translation_key = conv.prop.friendly_name
|
||||
self._miot_service = conv.prop.service
|
||||
self._miot_property = conv.prop
|
||||
if not conv.prop.readable:
|
||||
self._attr_available = True
|
||||
|
||||
elif isinstance(conv, MiotActionConv):
|
||||
self.entity_id = device.spec.generate_entity_id(self, conv.action.name, conv.domain)
|
||||
self._attr_name = str(conv.action.friendly_desc)
|
||||
self._attr_translation_key = conv.action.friendly_name
|
||||
self._miot_service = conv.action.service
|
||||
self._miot_action = conv.action
|
||||
self._miot_property = conv.prop
|
||||
self._attr_available = True
|
||||
|
||||
else:
|
||||
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_available = True
|
||||
|
||||
self.listen_attrs = {self.attr} | set(conv.attrs)
|
||||
if getattr(self, '_attr_name', None):
|
||||
self._attr_name = self._attr_name.replace(device.name, '').strip()
|
||||
self._attr_unique_id = f'{device.unique_id}-{convert_unique_id(conv)}'
|
||||
self._attr_device_info = self.device.hass_device_info
|
||||
self._attr_extra_state_attributes = {}
|
||||
|
||||
self._attr_icon = conv.option.get('icon')
|
||||
self._attr_device_class = self.custom_config('device_class') or conv.option.get('device_class')
|
||||
|
||||
if self._attr_translation_key:
|
||||
self._attr_translation_key = ( # hassfest
|
||||
self._attr_translation_key
|
||||
.replace(':', '-')
|
||||
.replace('.', '-')
|
||||
)
|
||||
|
||||
cate = self.custom_config('entity_category') or conv.option.get('entity_category')
|
||||
if isinstance(cate, str):
|
||||
try:
|
||||
cate = EntityCategory(cate)
|
||||
except Exception:
|
||||
cate = None
|
||||
if cate:
|
||||
self._attr_entity_category = cate
|
||||
else:
|
||||
cats = {
|
||||
'configuration_entities': EntityCategory.CONFIG,
|
||||
'diagnostic_entities': EntityCategory.DIAGNOSTIC,
|
||||
}
|
||||
for k, v in cats.items():
|
||||
names = self.custom_config_list(k) or []
|
||||
if self._miot_property and self._miot_property.in_list(names):
|
||||
self._attr_entity_category = v
|
||||
break
|
||||
if self._miot_action and self._miot_action.in_list(names):
|
||||
self._attr_entity_category = v
|
||||
break
|
||||
|
||||
self.on_init()
|
||||
self.device.add_listener(self.on_device_update)
|
||||
|
||||
@cached_property
|
||||
def model(self):
|
||||
return self.device.model
|
||||
|
||||
@cached_property
|
||||
def unique_mac(self):
|
||||
return self.device.info.unique_id
|
||||
|
||||
def on_init(self):
|
||||
"""Run on class init."""
|
||||
|
||||
async def async_update(self):
|
||||
await self.device.update_status()
|
||||
|
||||
def on_device_update(self, data: dict, only_info=False):
|
||||
state_change = False
|
||||
available = self.device.available
|
||||
|
||||
if not only_info:
|
||||
pass
|
||||
elif isinstance(self.conv, InfoConv):
|
||||
self._attr_available = available = True
|
||||
self._attr_icon = data.get('icon', self._attr_icon)
|
||||
old_data = self._attr_extra_state_attributes.copy()
|
||||
old_data.pop('updated_at', None)
|
||||
if data == old_data:
|
||||
return
|
||||
data['updated_at'] = str(self.device.data.get('updated', ''))
|
||||
self._attr_extra_state_attributes = data
|
||||
else:
|
||||
return
|
||||
|
||||
if keys := self.listen_attrs & data.keys():
|
||||
self._attr_available = available
|
||||
self.set_state(data)
|
||||
state_change = True
|
||||
for key in keys:
|
||||
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()
|
||||
state = data.get(self.attr, data)
|
||||
_LOGGER.debug('%s: Entity state updated: %s', self.entity_id, state)
|
||||
|
||||
def get_state(self) -> dict:
|
||||
"""Run before entity remove if entity is subclass from RestoreEntity."""
|
||||
return {}
|
||||
|
||||
def set_state(self, data: dict):
|
||||
"""Run on data from device."""
|
||||
self._attr_state = data.get(self.attr)
|
||||
|
||||
@property
|
||||
def extra_restore_state_data(self) -> ExtraStoredData | None:
|
||||
# filter None values
|
||||
if state := {k: v for k, v in self.get_state().items() if v is not None}:
|
||||
return RestoredExtraData(state)
|
||||
return None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
self.added = True
|
||||
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
|
||||
|
||||
if call := getattr(self, 'async_get_last_extra_data', None):
|
||||
data: RestoredExtraData = await call()
|
||||
if data and self.listen_attrs & data.as_dict().keys():
|
||||
self.set_state(data.as_dict())
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
self.device.remove_listener(self.on_device_update)
|
||||
|
||||
@cached_property
|
||||
def customize_keys(self):
|
||||
keys = []
|
||||
prop = getattr(self.conv, 'prop', None)
|
||||
action = getattr(self.conv, 'action', None)
|
||||
for mod in wildcard_models(self.device.model):
|
||||
if isinstance(action, MiotAction):
|
||||
keys.append(f'{mod}:{action.full_name}')
|
||||
keys.append(f'{mod}:{action.name}')
|
||||
if isinstance(prop, MiotProperty):
|
||||
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.conv.attr}')
|
||||
return keys
|
||||
|
||||
|
||||
def convert_unique_id(conv: 'BaseConv'):
|
||||
service = getattr(conv, 'service', None)
|
||||
if isinstance(conv, MiotServiceConv) and isinstance(service, MiotService):
|
||||
return service.iid
|
||||
|
||||
action = getattr(conv, 'action', None)
|
||||
if isinstance(action, MiotAction):
|
||||
return action.unique_name
|
||||
|
||||
prop = getattr(conv, 'prop', None)
|
||||
if isinstance(prop, MiotProperty):
|
||||
return prop.unique_name
|
||||
|
||||
return conv.attr
|
||||
@@ -0,0 +1,139 @@
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_USERNAME
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import SUPPORTED_DOMAINS
|
||||
from .xiaomi_cloud import MiotCloud
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class HassEntry:
|
||||
ALL: dict[str, 'HassEntry'] = {}
|
||||
cloud: MiotCloud = None
|
||||
cloud_devices = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
|
||||
self.id = entry.entry_id
|
||||
self.hass = hass
|
||||
self.entry = entry
|
||||
self.adders: dict[str, AddEntitiesCallback] = {}
|
||||
self.devices: dict[str, 'Device'] = {}
|
||||
self.mac_to_did = {}
|
||||
self.did_to_unique = {}
|
||||
|
||||
@staticmethod
|
||||
def init(hass: HomeAssistant, entry: ConfigEntry):
|
||||
this = HassEntry.ALL.get(entry.entry_id)
|
||||
if not this:
|
||||
this = HassEntry(hass, entry)
|
||||
HassEntry.ALL[entry.entry_id] = this
|
||||
return this
|
||||
|
||||
async def async_unload(self):
|
||||
ret = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
self.hass.config_entries.async_forward_entry_unload(self.entry, domain)
|
||||
for domain in SUPPORTED_DOMAINS
|
||||
]
|
||||
)
|
||||
)
|
||||
if ret:
|
||||
for device in self.devices.values():
|
||||
await device.async_unload()
|
||||
HassEntry.ALL.pop(self.entry.entry_id, None)
|
||||
return ret
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.entry, item)
|
||||
|
||||
@property
|
||||
def setup_in_progress(self):
|
||||
return self.entry.state == ConfigEntryState.SETUP_IN_PROGRESS
|
||||
|
||||
def get_config(self, key=None, default=None):
|
||||
dat = {
|
||||
**self.entry.data,
|
||||
**self.entry.options,
|
||||
}
|
||||
if self.filter_models:
|
||||
dat.pop('filter_did', None)
|
||||
dat.pop('did_list', None)
|
||||
else:
|
||||
dat.pop('filter_model', None)
|
||||
dat.pop('model_list', None)
|
||||
if key:
|
||||
return dat.get(key, default)
|
||||
return dat
|
||||
|
||||
@property
|
||||
def filter_models(self):
|
||||
data = {
|
||||
**self.entry.data,
|
||||
**self.entry.options,
|
||||
}
|
||||
if data.get('did_list'):
|
||||
return False
|
||||
if data.get('model_list'):
|
||||
return True
|
||||
if 'did_list' in data:
|
||||
return False
|
||||
if 'model_list' in data:
|
||||
return True
|
||||
return data.get('filter_models', False)
|
||||
|
||||
async def new_device(self, device_info: dict):
|
||||
from .device import Device, DeviceInfo
|
||||
info = DeviceInfo(device_info)
|
||||
if device := self.devices.get(info.unique_id):
|
||||
return device
|
||||
device = Device(info, self)
|
||||
self.devices[info.unique_id] = device
|
||||
self.did_to_unique[info.did] = info.unique_id
|
||||
await device.async_init()
|
||||
return device
|
||||
|
||||
def new_adder(self, domain, adder: AddEntitiesCallback):
|
||||
self.adders[domain] = adder
|
||||
_LOGGER.info('New adder: %s', [domain, adder])
|
||||
|
||||
for device in self.devices.values():
|
||||
device.add_entities(domain)
|
||||
|
||||
return self
|
||||
|
||||
async def get_cloud(self, check=False, login=False):
|
||||
if not self.cloud:
|
||||
if not self.get_config(CONF_USERNAME):
|
||||
return None
|
||||
self.cloud = await MiotCloud.from_token(self.hass, self.get_config(), login=login)
|
||||
if check:
|
||||
await self.cloud.async_check_auth(notify=True)
|
||||
return self.cloud
|
||||
|
||||
async def get_cloud_devices(self):
|
||||
if isinstance(self.cloud_devices, dict):
|
||||
return self.cloud_devices
|
||||
cloud = await self.get_cloud()
|
||||
if not cloud:
|
||||
return {}
|
||||
config = self.get_config()
|
||||
self.cloud_devices = await cloud.async_get_devices_by_key('did', filters=config) or {}
|
||||
for did, info in self.cloud_devices.items():
|
||||
mac = info.get('mac') or did
|
||||
self.mac_to_did[mac] = did
|
||||
return self.cloud_devices
|
||||
|
||||
async def get_cloud_device(self, did=None, mac=None):
|
||||
devices = await self.get_cloud_devices()
|
||||
if mac and not did:
|
||||
did = self.mac_to_did.get(mac)
|
||||
if did:
|
||||
return devices.get(did)
|
||||
return None
|
||||
@@ -0,0 +1,297 @@
|
||||
import time
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from typing import Tuple
|
||||
|
||||
from .utils import (
|
||||
DeviceException,
|
||||
is_offline_exception,
|
||||
rgb_to_int,
|
||||
int_to_rgb,
|
||||
)
|
||||
from .templates import template
|
||||
from .miot_spec import (MiotSpec, MiotProperty, MiotAction)
|
||||
from .miio2miot_specs import MIIO_TO_MIOT_SPECS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Miio2MiotHelper:
|
||||
def __init__(self, hass, config: dict, miot_spec: MiotSpec, from_model=None):
|
||||
self.hass = hass
|
||||
if ext := config.get('extend_model'):
|
||||
if m2m := Miio2MiotHelper.from_model(hass, ext, miot_spec):
|
||||
sps = {**m2m.config.get('miio_specs', {})}
|
||||
sps.update(config.get('miio_specs', {}))
|
||||
config = {**m2m.config, **config, 'miio_specs': sps}
|
||||
self.config = config
|
||||
self.miot_spec = miot_spec
|
||||
self.specs = config.get('miio_specs', {})
|
||||
self.model = from_model or config.get('model', None)
|
||||
self.miio_props = []
|
||||
for k, v in self.specs.items():
|
||||
if p := v.get('prop'):
|
||||
self.miio_props.append(p)
|
||||
self.extend_miio_props(config.get('miio_props', []))
|
||||
self.miio_props_values = {}
|
||||
|
||||
@staticmethod
|
||||
def from_model(hass, model, miot_spec, from_model=None):
|
||||
if not from_model:
|
||||
from_model = model
|
||||
cfg = MIIO_TO_MIOT_SPECS.get(model) or {}
|
||||
if isinstance(cfg, str):
|
||||
return Miio2MiotHelper.from_model(hass, cfg, miot_spec, from_model)
|
||||
if cfg:
|
||||
cfg.setdefault('model', model)
|
||||
return Miio2MiotHelper(hass, cfg, miot_spec, from_model)
|
||||
return None
|
||||
|
||||
def extend_miio_props(self, props: list):
|
||||
self.miio_props.extend(props)
|
||||
self.miio_props = list(dict(zip(self.miio_props, self.miio_props)).keys())
|
||||
return self.miio_props
|
||||
|
||||
async def async_get_miio_props(self, device):
|
||||
dic = {}
|
||||
if not self.config.get('without_props'):
|
||||
try:
|
||||
num = int(self.config.get('chunk_properties'))
|
||||
except (TypeError, ValueError):
|
||||
num = None
|
||||
try:
|
||||
vls = await device.async_get_prop(self.miio_props, max_properties=num)
|
||||
dic.update(dict(zip(self.miio_props, vls)))
|
||||
except (DeviceException, OSError) as exc:
|
||||
if is_offline_exception(exc):
|
||||
raise exc
|
||||
_LOGGER.error('%s: Got MiioException: %s while get_prop(%s)', self.model, exc, self.miio_props)
|
||||
except TypeError:
|
||||
_LOGGER.error('%s: Got TypeError while get_prop(%s)', self.model, self.miio_props, exc_info=True)
|
||||
if cls := self.config.get('miio_commands'):
|
||||
for c in cls:
|
||||
if dly := c.get('delay', 0):
|
||||
time.sleep(dly)
|
||||
pms = c.get('params', [])
|
||||
try:
|
||||
vls = await device.async_send(c['method'], pms)
|
||||
except (DeviceException, OSError, TypeError, UnboundLocalError) as exc:
|
||||
if is_offline_exception(exc):
|
||||
raise exc
|
||||
if not c.get('ignore_error'):
|
||||
_LOGGER.error('%s: Got MiioException: %s while %s(%s)', self.model, exc, c['method'], pms)
|
||||
continue
|
||||
kls = c.get('values', [])
|
||||
if kls is True:
|
||||
kls = c.get('params', [])
|
||||
if tpl := c.get('template'):
|
||||
tpl = template(tpl, self.hass)
|
||||
pdt = tpl.async_render({'results': vls})
|
||||
if isinstance(pdt, dict):
|
||||
dic.update(pdt)
|
||||
elif kls:
|
||||
if isinstance(vls, dict):
|
||||
vls = list(vls.values())
|
||||
i = 0
|
||||
for k in kls:
|
||||
if i >= len(vls):
|
||||
break
|
||||
dic[k] = vls[i]
|
||||
i += 1
|
||||
if tpl := self.config.get('miio_template'):
|
||||
tpl = template(tpl, self.hass)
|
||||
pdt = tpl.async_render({'props': dic})
|
||||
if isinstance(pdt, dict):
|
||||
dic.update(pdt)
|
||||
self.miio_props_values = dic
|
||||
_LOGGER.debug('%s: Got miio props for miot: %s', self.model, dic)
|
||||
return dic
|
||||
|
||||
async def async_get_miot_props(self, device, mapping: dict = None):
|
||||
if mapping is None:
|
||||
mapping = device.mapping or {}
|
||||
rls = []
|
||||
if dic := await self.async_get_miio_props(device):
|
||||
for k, v in mapping.items():
|
||||
u = MiotSpec.unique_prop(v, valid=True)
|
||||
c = self.specs.get(u, {})
|
||||
p = c.get('prop')
|
||||
if not p:
|
||||
continue
|
||||
val = dic.get(p)
|
||||
prop = self.miot_spec.specs.get(u)
|
||||
if prop and isinstance(prop, MiotProperty):
|
||||
mph = MiioPropertyHelper(prop)
|
||||
fmt = c.get('format')
|
||||
try:
|
||||
if tpl := c.get('template', {}):
|
||||
tpl = template(tpl, self.hass)
|
||||
val = tpl.async_render({
|
||||
'value': val,
|
||||
'props': dic,
|
||||
'dict': c.get('dict', {}),
|
||||
'min': prop.range_min(),
|
||||
'max': prop.range_max(),
|
||||
'step': prop.range_step(),
|
||||
'description': prop.list_description(val) if prop.value_list else None,
|
||||
})
|
||||
|
||||
elif fmt and hasattr(mph, fmt):
|
||||
val = getattr(mph, fmt)(val)
|
||||
|
||||
elif d := c.get('dict', {}):
|
||||
val = d.get(val, c.get('default', val))
|
||||
|
||||
elif prop.format in ['bool']:
|
||||
val = cv.boolean(val)
|
||||
|
||||
elif r := c.get('value_ratio'):
|
||||
val = round(float(val) * float(r), 3)
|
||||
|
||||
elif prop.format in ['float']:
|
||||
val = round(float(val), 4)
|
||||
|
||||
elif prop.is_integer:
|
||||
val = int(val)
|
||||
|
||||
except (TypeError, ValueError, vol.Invalid):
|
||||
val = dic.get(p)
|
||||
rls.append({
|
||||
**v,
|
||||
'code': 0,
|
||||
'value': val,
|
||||
})
|
||||
return rls
|
||||
|
||||
def has_setter(self, siid, piid=None, aiid=None):
|
||||
key = MiotSpec.unique_prop(siid=siid, piid=piid, aiid=aiid)
|
||||
ret = self.specs.get(key, {}).get('setter')
|
||||
return ret
|
||||
|
||||
async def async_set_property(self, device, siid, piid, value):
|
||||
key = MiotSpec.unique_prop(siid=siid, piid=piid)
|
||||
cfg = self.specs.get(key, {})
|
||||
setter = cfg.get('setter')
|
||||
if setter is True:
|
||||
setter = None
|
||||
if prop := cfg.get('prop'):
|
||||
setter = f'set_{prop}'
|
||||
pms = [value]
|
||||
prop = self.miot_spec.specs.get(key)
|
||||
if prop and isinstance(prop, MiotProperty):
|
||||
mph = MiioPropertyHelper(prop, reverse=True)
|
||||
fmt = cfg.get('format')
|
||||
if tpl := cfg.get('set_template'):
|
||||
tpl = template(tpl, self.hass)
|
||||
pms = tpl.async_render({
|
||||
'value': value,
|
||||
'props': self.miio_props_values,
|
||||
'dict': cfg.get('dict', {}),
|
||||
'min': prop.range_min(),
|
||||
'max': prop.range_max(),
|
||||
'step': prop.range_step(),
|
||||
'description': prop.list_description(value) if prop.value_list else None,
|
||||
}) or []
|
||||
if isinstance(pms, dict) and 'method' in pms:
|
||||
setter = pms.get('method', setter)
|
||||
pms = pms.get('params', [])
|
||||
elif fmt and hasattr(mph, fmt):
|
||||
pms = [getattr(mph, fmt)(value)]
|
||||
elif d := cfg.get('dict', {}):
|
||||
for dk, dv in d.items():
|
||||
if dv == value:
|
||||
pms = [dk]
|
||||
break
|
||||
pms = cv.ensure_list(pms)
|
||||
if not setter:
|
||||
_LOGGER.warning('%s: Set miio prop via miot failed: %s', self.model, [key, setter, cfg])
|
||||
return None
|
||||
_LOGGER.info('%s: Set miio prop via miot: %s', self.model, [key, setter, pms])
|
||||
ret = await device.async_send(setter, pms) or ['']
|
||||
iok = ret == ['ok']
|
||||
if self.config.get('ignore_result'):
|
||||
iok = ret or isinstance(ret, list)
|
||||
cbk = cfg.get('set_callback')
|
||||
if iok and cbk:
|
||||
cbk(prop=cfg.get('prop'), config=cfg, setter=setter, params=pms, props=self.miio_props_values)
|
||||
return {
|
||||
'code': 0 if iok else 1,
|
||||
'siid': siid,
|
||||
'piid': piid,
|
||||
'result': ret,
|
||||
}
|
||||
|
||||
async def async_call_action(self, device, siid, aiid, params):
|
||||
key = MiotSpec.unique_prop(siid=siid, aiid=aiid)
|
||||
cfg = self.specs.get(key, {})
|
||||
setter = cfg.get('setter')
|
||||
pms = cv.ensure_list(params)
|
||||
act = self.miot_spec.specs.get(key)
|
||||
if act and isinstance(act, MiotAction):
|
||||
if tpl := cfg.get('set_template'):
|
||||
tpl = template(tpl, self.hass)
|
||||
pms = tpl.async_render({
|
||||
'params': pms,
|
||||
'props': self.miio_props_values,
|
||||
}) or []
|
||||
if isinstance(pms, dict) and 'method' in pms:
|
||||
setter = pms.get('method', setter)
|
||||
pms = pms.get('params', [])
|
||||
pms = cv.ensure_list(pms)
|
||||
if not setter:
|
||||
_LOGGER.warning('%s: Call miio method via miot action failed: %s', self.model, [key, setter, cfg])
|
||||
return None
|
||||
_LOGGER.info('%s: Call miio method via miot action: %s', self.model, [key, setter, pms])
|
||||
ret = await device.async_send(setter, pms) or ['']
|
||||
iok = ret == ['ok']
|
||||
if self.config.get('ignore_result'):
|
||||
iok = ret or isinstance(ret, list)
|
||||
return {
|
||||
'code': 0 if iok else 1,
|
||||
'siid': siid,
|
||||
'aiid': aiid,
|
||||
'result': ret,
|
||||
}
|
||||
|
||||
def entity_attrs(self):
|
||||
adt = {}
|
||||
eas = self.config.get('entity_attrs', [])
|
||||
if isinstance(eas, list):
|
||||
eas = {
|
||||
k: k
|
||||
for k in eas
|
||||
}
|
||||
for k, p in eas.items():
|
||||
v = self.miio_props_values.get(p)
|
||||
if v is not None:
|
||||
adt[k] = v
|
||||
return adt
|
||||
|
||||
def only_miio_props(self, props: list):
|
||||
rls = []
|
||||
for p in props:
|
||||
rls.append(self.miio_props_values.get(p))
|
||||
return rls
|
||||
|
||||
|
||||
class MiioPropertyHelper:
|
||||
def __init__(self, prop: MiotProperty, reverse=False):
|
||||
self.property = prop
|
||||
self.reverse = reverse
|
||||
|
||||
def onoff(self, value):
|
||||
if self.reverse:
|
||||
return 'on' if value else 'off'
|
||||
return value is True or 'on' == f'{value}'.lower()
|
||||
|
||||
def rgb(self, value):
|
||||
is_tuple = isinstance(value, Tuple)
|
||||
if self.reverse:
|
||||
if not is_tuple:
|
||||
value = int_to_rgb(value)
|
||||
value = list(value)
|
||||
elif is_tuple:
|
||||
return rgb_to_int(value)
|
||||
return value
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,353 @@
|
||||
# https://github.com/AlexxIT/XiaomiGateway3/blob/02aaddc3c16f63df116a95578f44d5b0da1c4f2c/custom_components/xiaomi_gateway3/core/mini_miio.py
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import socket
|
||||
import time
|
||||
from asyncio import DatagramProtocol, Future
|
||||
from asyncio.protocols import BaseProtocol
|
||||
from asyncio.transports import DatagramTransport
|
||||
from typing import Union
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
_LOGGER = logging.getLogger(__package__ + ".miio")
|
||||
|
||||
HELLO = bytes.fromhex(
|
||||
"21310020ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
)
|
||||
|
||||
|
||||
class BasemiIO:
|
||||
"""A simple class that implements the miIO protocol."""
|
||||
|
||||
device_id = None
|
||||
delta_ts = None
|
||||
debug = False
|
||||
|
||||
def __init__(self, host: str, token: str, timeout: float = 3):
|
||||
self.addr = (host, 54321)
|
||||
self.token = bytes.fromhex(token)
|
||||
self.timeout = timeout
|
||||
|
||||
key = hashlib.md5(self.token).digest()
|
||||
iv = hashlib.md5(key + self.token).digest()
|
||||
self.cipher = Cipher(
|
||||
algorithms.AES(key), modes.CBC(iv), backend=default_backend()
|
||||
)
|
||||
|
||||
def _encrypt(self, plaintext: bytes):
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_plaintext = padder.update(plaintext) + padder.finalize()
|
||||
|
||||
encryptor = self.cipher.encryptor()
|
||||
return encryptor.update(padded_plaintext) + encryptor.finalize()
|
||||
|
||||
def _decrypt(self, ciphertext: bytes):
|
||||
decryptor = self.cipher.decryptor()
|
||||
padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
||||
|
||||
unpadder = padding.PKCS7(128).unpadder()
|
||||
return unpadder.update(padded_plaintext) + unpadder.finalize()
|
||||
|
||||
def _pack_raw(self, msg_id: int, method: str, params: Union[dict, list] = None):
|
||||
# latest zero unnecessary
|
||||
payload = (
|
||||
json.dumps(
|
||||
{"id": msg_id, "method": method, "params": params or []},
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
+ b"\x00"
|
||||
)
|
||||
|
||||
data = self._encrypt(payload)
|
||||
|
||||
raw = b"\x21\x31"
|
||||
raw += (32 + len(data)).to_bytes(2, "big") # total length
|
||||
raw += b"\x00\x00\x00\x00" # unknow
|
||||
raw += self.device_id.to_bytes(4, "big")
|
||||
raw += int(time.time() - self.delta_ts).to_bytes(4, "big")
|
||||
|
||||
raw += hashlib.md5(raw + self.token + data).digest()
|
||||
raw += data
|
||||
|
||||
assert len(raw) < 1024, "Exceeded message size"
|
||||
|
||||
return raw
|
||||
|
||||
def _unpack_raw(self, raw: bytes):
|
||||
assert raw[:2] == b"\x21\x31"
|
||||
# length = int.from_bytes(raw[2:4], 'big')
|
||||
# unknown = raw[4:8]
|
||||
# device_id = int.from_bytes(raw[8:12], 'big')
|
||||
# ts = int.from_bytes(raw[12:16], 'big')
|
||||
# checksum = raw[16:32]
|
||||
return self._decrypt(raw[32:])
|
||||
|
||||
|
||||
class SyncMiIO(BasemiIO):
|
||||
"""Synchronous miIO protocol."""
|
||||
|
||||
def ping(self, sock: socket) -> bool:
|
||||
"""Returns `true` if the connection to the miio device is working. The
|
||||
token is not verified at this stage.
|
||||
"""
|
||||
try:
|
||||
sock.sendto(HELLO, self.addr)
|
||||
raw = sock.recv(1024)
|
||||
if raw[:2] == b"\x21\x31":
|
||||
self.device_id = int.from_bytes(raw[8:12], "big")
|
||||
self.delta_ts = time.time() - int.from_bytes(raw[12:16], "big")
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def send(self, method: str, params: Union[dict, list] = None):
|
||||
"""Send command to miIO device and get result from it. Params can be
|
||||
dict or list depend on command.
|
||||
"""
|
||||
pings = 0
|
||||
for times in range(1, 4):
|
||||
try:
|
||||
# create socket every time for reset connection, because we can
|
||||
# reseive answer on previous request or request from another
|
||||
# thread
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(self.timeout)
|
||||
|
||||
# need device_id for send command, can get it from ping cmd
|
||||
if self.delta_ts is None and not self.ping(sock):
|
||||
pings += 1
|
||||
continue
|
||||
|
||||
# pack each time for new message id
|
||||
msg_id = random.randint(100000000, 999999999)
|
||||
raw_send = self._pack_raw(msg_id, method, params)
|
||||
t = time.monotonic()
|
||||
sock.sendto(raw_send, self.addr)
|
||||
# can receive more than 1024 bytes (1056 approximate maximum)
|
||||
raw_recv = sock.recv(10240)
|
||||
t = time.monotonic() - t
|
||||
data = self._unpack_raw(raw_recv).rstrip(b"\x00")
|
||||
|
||||
if data == b"":
|
||||
# mgl03 fw 1.4.6_0012 without Internet respond on miIO.info
|
||||
# command with empty answer
|
||||
data = {"result": ""}
|
||||
break
|
||||
|
||||
data = json.loads(data)
|
||||
# check if we received response for our cmd
|
||||
if data["id"] == msg_id:
|
||||
break
|
||||
|
||||
_LOGGER.debug(f"{self.addr[0]} | wrong ID")
|
||||
|
||||
except socket.timeout:
|
||||
_LOGGER.debug(f"{self.addr[0]} | timeout {times}")
|
||||
except Exception as e:
|
||||
_LOGGER.debug(f"{self.addr[0]}", exc_info=e)
|
||||
|
||||
# init ping again
|
||||
self.delta_ts = None
|
||||
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
f"{self.addr[0]} | Device offline"
|
||||
if pings >= 2
|
||||
else f"{self.addr[0]} | Can't send {method} {params}"
|
||||
)
|
||||
return None
|
||||
|
||||
if self.debug:
|
||||
_LOGGER.debug(
|
||||
f"{self.addr[0]} | Send {method} {len(raw_send)}B, "
|
||||
f"recv {len(raw_recv)}B in {t:.1f} sec and {times} try"
|
||||
)
|
||||
|
||||
if "result" in data:
|
||||
return data["result"]
|
||||
else:
|
||||
_LOGGER.debug(f"{self.addr[0]} | {data}")
|
||||
return None
|
||||
|
||||
def send_bulk(self, method: str, params: list):
|
||||
"""Sends a command with a large number of parameters. Splits into
|
||||
multiple requests when the size of one request is exceeded.
|
||||
"""
|
||||
try:
|
||||
result = []
|
||||
# Chunk of 15 is seems like the best size. Because request should
|
||||
# be lower than 1024 and response should be not more than 1056.
|
||||
# {'did':'1234567890','siid': 2,'piid': 1,'value': False,'code': 0}
|
||||
for i in range(0, len(params), 15):
|
||||
result += self.send(method, params[i : i + 15])
|
||||
return result
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def info(self) -> Union[dict, str, None]:
|
||||
"""Get info about miIO device.
|
||||
|
||||
Response dict - device ok, token ok
|
||||
Response empty string - device ok, token ok (mgl03 on fw 1.4.6_0012
|
||||
without cloud connection)
|
||||
Response None, device_id not None - device ok, token wrong
|
||||
Response None, device_id None - device offline
|
||||
"""
|
||||
return self.send("miIO.info")
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
class AsyncSocket(DatagramProtocol):
|
||||
timeout = 0
|
||||
transport: DatagramTransport = None
|
||||
response: Future = None
|
||||
|
||||
def __init__(self):
|
||||
self.response = asyncio.get_event_loop().create_future()
|
||||
|
||||
def connection_made(self, transport: DatagramTransport):
|
||||
# _LOGGER.debug(f"connection_made {transport}")
|
||||
self.transport = transport
|
||||
|
||||
def datagram_received(self, data: bytes, addr):
|
||||
# _LOGGER.debug(f"datagram_received {data}")
|
||||
self.response.set_result(data)
|
||||
|
||||
def settimeout(self, value: int):
|
||||
self.timeout = value
|
||||
|
||||
def sendto(self, data: bytes):
|
||||
self.transport.sendto(data)
|
||||
|
||||
def close(self):
|
||||
if not self.transport:
|
||||
return
|
||||
try:
|
||||
self.transport.close()
|
||||
except Exception as e:
|
||||
_LOGGER.error("Error when closing async socket", exc_info=e)
|
||||
|
||||
async def connect(self, addr: tuple[str, int]):
|
||||
coro = asyncio.get_event_loop().create_datagram_endpoint(
|
||||
lambda: self, remote_addr=addr
|
||||
)
|
||||
if self.timeout:
|
||||
await asyncio.wait_for(coro, self.timeout)
|
||||
else:
|
||||
await coro
|
||||
|
||||
async def recv(self, *args):
|
||||
self.response = asyncio.get_event_loop().create_future()
|
||||
if self.timeout:
|
||||
return await asyncio.wait_for(self.response, self.timeout)
|
||||
return await self.response
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic,PyTypeChecker
|
||||
class AsyncMiIO(BasemiIO, BaseProtocol):
|
||||
async def ping(self, sock: AsyncSocket) -> bool:
|
||||
"""Returns `true` if the connection to the miio device is working. The
|
||||
token is not verified at this stage.
|
||||
"""
|
||||
try:
|
||||
sock.sendto(HELLO)
|
||||
raw = await sock.recv(1024)
|
||||
if raw[:2] == b"\x21\x31":
|
||||
self.device_id = int.from_bytes(raw[8:12], "big")
|
||||
self.delta_ts = time.time() - int.from_bytes(raw[12:16], "big")
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
async def send(self, method: str, params: Union[dict, list] = None, tries=3):
|
||||
"""Send command to miIO device and get result from it. Params can be
|
||||
dict or list depend on command.
|
||||
|
||||
Possible results:
|
||||
- None - device offline
|
||||
- {} (empty dict) - device online but can't response on cmd
|
||||
- {'id':123,'result':...} - device answered on cmd with good result
|
||||
- {'id':123,'error':...}
|
||||
"""
|
||||
offline = False
|
||||
for _ in range(0, tries):
|
||||
sock = AsyncSocket()
|
||||
sock.settimeout(self.timeout)
|
||||
try:
|
||||
# create socket every time for reset connection, because we can
|
||||
# reseive answer on previous request or request from another
|
||||
# thread
|
||||
await sock.connect(self.addr)
|
||||
|
||||
# need device_id for send command, can get it from ping cmd
|
||||
if self.delta_ts is None and not await self.ping(sock):
|
||||
# device doesn't answered on ping
|
||||
offline = True
|
||||
continue
|
||||
|
||||
# pack each time for new message id
|
||||
msg_id = random.randint(100000000, 999999999)
|
||||
raw_send = self._pack_raw(msg_id, method, params)
|
||||
sock.sendto(raw_send)
|
||||
# can receive more than 1024 bytes (1056 approximate maximum)
|
||||
raw_recv = await sock.recv(10240)
|
||||
data = self._unpack_raw(raw_recv).rstrip(b"\x00")
|
||||
|
||||
if data == b"":
|
||||
# mgl03 fw 1.4.6_0012 without Internet respond on miIO.info
|
||||
# command with empty answer
|
||||
continue
|
||||
|
||||
data = json.loads(data)
|
||||
# check if we received response for our cmd
|
||||
if data["id"] != msg_id:
|
||||
_LOGGER.debug(f"{self.addr[0]} | wrong answer ID")
|
||||
continue
|
||||
|
||||
return data
|
||||
|
||||
except (asyncio.TimeoutError, OSError):
|
||||
# OSError: [Errno 101] Network unreachable
|
||||
pass
|
||||
except Exception as e:
|
||||
_LOGGER.debug(f"{self.addr[0]} | {method}", exc_info=e)
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
# init ping again
|
||||
self.delta_ts = None
|
||||
|
||||
if offline:
|
||||
_LOGGER.debug(f"{self.addr[0]} | Device offline")
|
||||
return None
|
||||
|
||||
_LOGGER.debug(f"{self.addr[0]} | No answer on {method} {params}")
|
||||
return {}
|
||||
|
||||
async def send_bulk(self, method: str, params: list, chunk: int = 0) -> list:
|
||||
"""Sends a command with a large number of parameters. Splits into
|
||||
multiple requests when the size of one request is exceeded.
|
||||
"""
|
||||
if not chunk:
|
||||
chunk = 15
|
||||
try:
|
||||
result = []
|
||||
for i in range(0, len(params), chunk):
|
||||
resp = await self.send(method, params[i : i + chunk])
|
||||
result += resp["result"]
|
||||
return result
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def info(self, tries: int = 3) -> dict | None:
|
||||
"""Get info about miIO device."""
|
||||
resp = await self.send("miIO.info", tries=tries)
|
||||
return resp.get("result") if resp else resp
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,197 @@
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
CUSTOM_TEMPLATES = {
|
||||
# https://iot.mi.com/new/doc/accesses/direct-access/embedded-development/ble/object-definition#%E7%89%99%E5%88%B7%E4%BA%8B%E4%BB%B6
|
||||
'ble_toothbrush_events': "{%- set dat = props.get('event.16') | default('{}',true) | from_json %}"
|
||||
"{%- set tim = dat.timestamp | default(0,true) | timestamp_local %}"
|
||||
"{%- set val = dat.get('value',[]).0 | default('0000') %}"
|
||||
"{%- set typ = val[0:2] | int(0,16) %}"
|
||||
"{%- set num = val[2:4] | int(0,16) %}"
|
||||
"{{ {"
|
||||
"'event': 'start',"
|
||||
"'counter': num,"
|
||||
"'timestamp': tim,"
|
||||
"} if typ == 0 else {"
|
||||
"'event': 'finish',"
|
||||
"'score': num,"
|
||||
"'timestamp': tim,"
|
||||
"} }}",
|
||||
# https://iot.mi.com/new/doc/accesses/direct-access/embedded-development/ble/object-definition#%E9%94%81%E4%BA%8B%E4%BB%B6
|
||||
'ble_lock_events': "{%- set mark_data = props.get('event.6') | default('{}',true) | from_json %}"
|
||||
"{%- set mark = mark_data.get('value',[]).0 | default('') %}"
|
||||
"{%- set door_data = props.get('event.7') | default('{}',true) | from_json %}"
|
||||
"{%- set door = (door_data.get('value',[]).0 | default(''))[:2] | int(-1,16) %}"
|
||||
"{%- set fang_data = props.get('event.8') | default('{}',true) | from_json %}"
|
||||
"{%- set fang = (fang_data.get('value',[]).0 | default(''))[:2] | int(-1,16) %}"
|
||||
"{%- set lock_data = props.get('event.11') | default('{}',true) | from_json %}"
|
||||
"{%- set lock = lock_data.get('value',[]).0 | default('') %}"
|
||||
"{%- set lock_action = lock[:2] | int(-1,16) % 16 %}"
|
||||
"{%- set lock_method = lock[:2] | int(-1,16) // 16 %}"
|
||||
"{%- set lock_key = lock[2:10] %}"
|
||||
"{%- set lock_fault = lock_key if lock_key[:4] == 'c0de' else None %}"
|
||||
"{%- set key_id = (0).from_bytes((0).to_bytes(0,'little').fromhex(lock_key),'little') %}"
|
||||
"{%- set key_types = {'00000000':'admin','ffffffff':'unknown','deadbeef':'invalid'} %}"
|
||||
"{%- set key_results = ['success','fail','timeout','blurry','less','dry','wet'] %}"
|
||||
"{%- set door_states = ['open','close','close_timeout','knock','breaking','stuck'] %}"
|
||||
"{%- set lock_actions = ['outside_unlock','lock','anti_lock_on','anti_lock_off',"
|
||||
"'inside_unlock','lock_inside','child_lock_on','child_lock_off','lock_outside'] %}"
|
||||
"{%- set lock_methods = ['bluetooth','password','biological','key','turntable','nfc',"
|
||||
"'one_time_password','two_step_verification','coercion','homekit','manual','automatic'] %}"
|
||||
"{{ {"
|
||||
"'fingerprint_id': key_types[mark[:8]] | default(mark[:8]),"
|
||||
"'fingerprint_result': key_results[mark[8:10] | int(-1,16)] | default('unknown'),"
|
||||
"'door_state': door_states[door] | default('unknown'),"
|
||||
"'armed_state': true if fang > 0 else false,"
|
||||
"'lock_action': lock_actions[lock_action] | default('unknown'),"
|
||||
"'lock_method': lock_methods[lock_method] | default('unknown'),"
|
||||
"'lock_key': key_types[lock_key] | default(lock_key),"
|
||||
"'lock_key_id': key_id,"
|
||||
"'lock_fault': lock_fault | default('none',true),"
|
||||
"'lock_data': lock,"
|
||||
"'timestamp': lock_data.timestamp | default(0,true) | timestamp_local,"
|
||||
"} }}",
|
||||
# https://iot.mi.com/new/doc/accesses/direct-access/embedded-development/ble/object-definition#%E7%83%9F%E9%9B%BE%E5%B1%9E%E6%80%A7
|
||||
'ble_sensor_smoke': "{%- set val = props.get('prop.4117','00') | int(0,16) %}"
|
||||
"{{ {"
|
||||
"'smoke_status': val == 1,"
|
||||
"} }}",
|
||||
'chuangmi_plug_v3_power_cost': "{%- set val = (result.0 | default({})).get('value',{}) %}"
|
||||
"{%- set day = now().day %}"
|
||||
"{{ {"
|
||||
"'today': val.pc | default(0),"
|
||||
"'today_duration': val.pc_time | default(0),"
|
||||
"'month': result[:day] | sum(attribute='value.pc'),"
|
||||
"'month_duration': result[:day] | sum(attribute='value.pc_time'),"
|
||||
"} }}",
|
||||
'lock_event_7_template': "{%- set val = (result.0 | default({})).get('value','[-1]') %}"
|
||||
"{%- set val = (val | from_json).0 | string %}"
|
||||
"{%- set evt = val[:2] | int(-1,16) %}"
|
||||
"{%- set els = ['open','close','close_timeout',"
|
||||
"'knock','breaking','stuck','unknown'] %}"
|
||||
"{{ {"
|
||||
"'door_event': evt,"
|
||||
"'door_state': els[evt] | default('unknown'),"
|
||||
"} }}",
|
||||
'lock_event_11_template': "{%- set val = (result.0 | default({})).get('value','[-1]') %}"
|
||||
"{%- set val = (val | from_json).0 | string %}"
|
||||
"{%- set evt = val[:2] | int(-1,16) % 16 %}"
|
||||
"{%- set how = val[:2] | int(-1,16) // 16 %}"
|
||||
"{%- set key = (0).from_bytes((0).to_bytes(0,'little')"
|
||||
".fromhex(val[2:10]), 'little') %}"
|
||||
"{%- set els = ['outside_unlock','lock','anti_lock_on','anti_lock_off',"
|
||||
"'inside_unlock','lock_inside','child_lock_on','child_lock_off','unknown'] %}"
|
||||
"{%- set mls = ['bluetooth','password','biological','key','turntable',"
|
||||
"'nfc','one_time_password','two_step_verification','coercion','homekit',"
|
||||
"'manual','automatic','unknown'] %}"
|
||||
"{{ {"
|
||||
"'lock_event': evt,"
|
||||
"'lock_state': els[evt] | default('unknown'),"
|
||||
"'method_id': how,"
|
||||
"'method': mls[how] | default('unknown'),"
|
||||
"'key_id': key,"
|
||||
"} }}",
|
||||
'lumi_acpartner_electric_power': "{%- set val = props.get('prop.load_power') or props.get('prop.ac_power',0) %}"
|
||||
"{{ {"
|
||||
"'electric_power': val | default(0,true) | round(2),"
|
||||
"} }}",
|
||||
'lumi_acpartner_miio_status': "{%- set model = results[0] | default('') %}"
|
||||
"{%- set state = results[1] | default('') %}"
|
||||
"{{ {"
|
||||
"'power': state[2:3] | int(0) == 1,"
|
||||
"'mode': [3,1,0,2,4][state[3:4] | int(2)],"
|
||||
"'fan_level': [1,2,3,0][state[4:5] | int(3)],"
|
||||
"'vertical_swing': state[5:6] in ['0','C'],"
|
||||
"'target_temperature': state[6:8] | int(0,16),"
|
||||
"'load_power': results[2] | default(0) | float | round(2),"
|
||||
"} }}",
|
||||
'micloud_statistics_power_cost': "{%- set dat = namespace(today=0,month=0) %}"
|
||||
"{%- set tim = now() %}"
|
||||
"{%- set stm = tim - timedelta(minutes=tim.minute,seconds=tim.second) %}"
|
||||
"{%- set tod = stm - timedelta(hours=tim.hour) %}"
|
||||
"{%- set stm = tod - timedelta(days=tim.day-1) %}"
|
||||
"{%- set tod = tod | as_timestamp | int(0) %}"
|
||||
"{%- set stm = stm | as_timestamp | int(0) %}"
|
||||
"{%- for d in (result or []) %}"
|
||||
"{%- set t = d.time | default(0) | int(0) %}"
|
||||
"{%- if t >= stm %}"
|
||||
"{%- set v = (d.value | default('[]') | string | from_json) or [] %}"
|
||||
"{%- set n = v[0] | default(0) %}"
|
||||
"{%- if t >= tod %}"
|
||||
"{%- set dat.today = n %}"
|
||||
"{%- endif %}"
|
||||
"{%- set dat.month = dat.month + n %}"
|
||||
"{%- endif %}"
|
||||
"{%- endfor %}"
|
||||
"{{ {"
|
||||
"'power_cost_today': dat.today | round(3),"
|
||||
"'power_cost_month': dat.month | round(3),"
|
||||
"} }}",
|
||||
'midr_rv_mirror_cloud_props': "{%- set sta = props.get('prop.Status',0) | int %}"
|
||||
"{%- set pos = props.get('prop.Position','{}') | from_json %}"
|
||||
"{%- set tim = pos.get('up_time_stamp',0) | int %}"
|
||||
"{{ {"
|
||||
"'prop.status': sta,"
|
||||
"'prop.position': pos,"
|
||||
"'prop.update_at': (tim / 1000) | timestamp_local,"
|
||||
"} }}",
|
||||
'mxiang_cateye_cloud_props': "{{ {"
|
||||
"'battery_level': props.get('prop.battery_level','0') | from_json | int,"
|
||||
"'is_can_open_video': props.get('prop.is_can_open_video','0') | from_json | int,"
|
||||
"} }}",
|
||||
'mxiang_cateye_human_visit_details': "{%- set val = (result.0 | default({})).get('value','{}') %}"
|
||||
"{%- set val = val | from_json %}"
|
||||
"{{ {"
|
||||
"'motion_video_time': val.get('visitTime',0) | timestamp_local,"
|
||||
"'motion_video_type': val.get('action'),"
|
||||
"'motion_video_latest': val,"
|
||||
"'_entity_attrs': True,"
|
||||
"} }}",
|
||||
'yeelink_bhf_light_v2_fan_levels': "{%- set val = ('00000' ~ value)[-5:] %}"
|
||||
"{%- set mds = {"
|
||||
"'drying_cloth': val[0],"
|
||||
"'coolwind': val[1],"
|
||||
"'drying': val[2],"
|
||||
"'venting': val[3],"
|
||||
"'warmwind': val[4],"
|
||||
"} %}"
|
||||
"{{ [1,2,3][mds[props.bh_mode] | default(0) | int(0)] | default(3) }}",
|
||||
'yeelink_bhf_light_v5_fan_levels': "{%- set val = ('000' ~ value)[-3:] %}"
|
||||
"{%- set mds = {"
|
||||
"'warmwind': val[0],"
|
||||
"'coolwind': val[1],"
|
||||
"'venting': val[2],"
|
||||
"} %}"
|
||||
"{{ [1,1,3,3][mds[props.bh_mode] | default(0) | int(0)] | default(1) }}",
|
||||
'yeelink_bhf_light_v5_miio_props': "{%- set val = ('000' ~ props.fan_speed_idx)[-3:] %}"
|
||||
"{{ {"
|
||||
"'warmwind_gear': val[0],"
|
||||
"'coolwind_gear': val[1],"
|
||||
"'venting_gear': val[2],"
|
||||
"} }}",
|
||||
'zimi_powerstrip_v2_power_cost': "{%- set val = (result.0 | default({})).get('value','[0]') %}"
|
||||
"{%- set day = now().day %}"
|
||||
"{%- set vls = (val | from_json)[0-day:] %}"
|
||||
"{%- set dat = namespace(today=0,month=0) %}"
|
||||
"{%- for v in vls %}"
|
||||
"{%- set v = (v | string).split(',') %}"
|
||||
"{%- if v[0] | default(0) | int(0) > 86400 %}"
|
||||
"{%- set dat.today = v[1] | default(0) | round(3) %}"
|
||||
"{%- set dat.month = dat.month + dat.today %}"
|
||||
"{%- endif %}"
|
||||
"{%- endfor %}"
|
||||
"{{ {"
|
||||
"'today': dat.today,"
|
||||
"'month': dat.month | round(3),"
|
||||
"} }}",
|
||||
}
|
||||
|
||||
|
||||
def template(value, hass):
|
||||
if value is None:
|
||||
raise ValueError('template value is None')
|
||||
if isinstance(value, (list, dict, Template)):
|
||||
raise TypeError('template value should be a string')
|
||||
value = CUSTOM_TEMPLATES.get(value, value)
|
||||
template_value = Template(str(value), hass)
|
||||
template_value.ensure_valid()
|
||||
return template_value
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,369 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import locale
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import tzlocal
|
||||
import logging
|
||||
import fnmatch
|
||||
import voluptuous as vol
|
||||
from typing import Type, Tuple, Optional, Callable, Set
|
||||
from functools import wraps
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import language as language_util
|
||||
from homeassistant.util.dt import DEFAULT_TIME_ZONE, get_time_zone
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DOMAIN, DEVICE_CUSTOMIZES, DATA_CUSTOMIZE
|
||||
from .translation_languages import TRANSLATION_LANGUAGES
|
||||
|
||||
|
||||
def get_value(obj, key, def_value=None, sep='.'):
|
||||
keys = f'{key}'.split(sep)
|
||||
result = obj
|
||||
for k in keys:
|
||||
if result is None:
|
||||
return None
|
||||
if isinstance(result, dict):
|
||||
result = result.get(k, def_value)
|
||||
elif isinstance(result, (list, tuple)):
|
||||
try:
|
||||
result = result[int(k)]
|
||||
except Exception:
|
||||
result = def_value
|
||||
return result
|
||||
|
||||
|
||||
def get_customize_via_model(model, key=None, default=None):
|
||||
cfg = {}
|
||||
for m in wildcard_models(model):
|
||||
cus = DEVICE_CUSTOMIZES.get(m) or {}
|
||||
if key is not None and key not in cus:
|
||||
continue
|
||||
if cus:
|
||||
cfg = {**cus, **cfg}
|
||||
return cfg if key is None else cfg.get(key, default)
|
||||
|
||||
|
||||
def get_customize_via_entity(entity, key=None, default=None):
|
||||
if key is None:
|
||||
default = {}
|
||||
if not isinstance(entity, Entity):
|
||||
return default
|
||||
cfg = {}
|
||||
if entity.hass and entity.entity_id:
|
||||
cfg = {
|
||||
**(entity.hass.data[DATA_CUSTOMIZE].get(entity.entity_id) or {}),
|
||||
**(entity.hass.data[DOMAIN].get(DATA_CUSTOMIZE, {}).get(entity.entity_id) or {}),
|
||||
}
|
||||
if key is not None and key in cfg:
|
||||
return cfg.get(key)
|
||||
mls = []
|
||||
if model := getattr(entity, 'model', None):
|
||||
if hasattr(entity, 'customize_keys'):
|
||||
mls.extend(entity.customize_keys)
|
||||
mls.append(model)
|
||||
for mod in mls:
|
||||
cus = get_customize_via_model(mod)
|
||||
cfg = {**cus, **cfg}
|
||||
return cfg if key is None else cfg.get(key, default)
|
||||
|
||||
|
||||
class CustomConfigHelper:
|
||||
def custom_config(self, key=None, default=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def custom_config_bool(self, key=None, default=None):
|
||||
val = self.custom_config(key, default)
|
||||
try:
|
||||
val = cv.boolean(val)
|
||||
except vol.Invalid:
|
||||
val = default
|
||||
return val
|
||||
|
||||
def custom_config_number(self, key=None, default=None):
|
||||
num = default
|
||||
val = self.custom_config(key)
|
||||
if val is not None:
|
||||
try:
|
||||
num = float(f'{val}')
|
||||
except (TypeError, ValueError):
|
||||
num = default
|
||||
return num
|
||||
|
||||
def custom_config_integer(self, key=None, default=None):
|
||||
num = self.custom_config_number(key, default)
|
||||
if num is not None:
|
||||
num = int(num)
|
||||
return num
|
||||
|
||||
def custom_config_list(self, key=None, default=None):
|
||||
lst = self.custom_config(key)
|
||||
if lst is None:
|
||||
return default
|
||||
if isinstance(lst, (int, float, bool)):
|
||||
lst = [lst]
|
||||
elif not isinstance(lst, list):
|
||||
lst = f'{lst}'.split(',')
|
||||
lst = list(map(lambda x: x.strip(), lst))
|
||||
return lst
|
||||
|
||||
def custom_config_json(self, key=None, default=None):
|
||||
dic = self.custom_config(key)
|
||||
if dic:
|
||||
if not isinstance(dic, (dict, list)):
|
||||
try:
|
||||
dic = json.loads(dic or '{}')
|
||||
except (TypeError, ValueError):
|
||||
dic = None
|
||||
if isinstance(dic, (dict, list)):
|
||||
return dic
|
||||
return default
|
||||
|
||||
|
||||
def get_manifest(field=None, default=None):
|
||||
manifest = {}
|
||||
with open(f'{os.path.dirname(__file__)}/../manifest.json') as fil:
|
||||
manifest = json.load(fil) or {}
|
||||
return manifest.get(field, default) if field else manifest
|
||||
|
||||
|
||||
async def async_get_manifest(hass: HomeAssistant, field=None, default=None):
|
||||
return await hass.async_add_executor_job(get_manifest, field, default)
|
||||
|
||||
|
||||
def local_zone(hass=None):
|
||||
try:
|
||||
if isinstance(hass, HomeAssistant):
|
||||
return get_time_zone(hass.config.time_zone)
|
||||
return tzlocal.get_localzone()
|
||||
except KeyError:
|
||||
pass
|
||||
return DEFAULT_TIME_ZONE
|
||||
|
||||
|
||||
def in_china(hass=None):
|
||||
if isinstance(hass, HomeAssistant):
|
||||
if hass.config.time_zone in ['Asia/Shanghai', 'Asia/Hong_Kong']:
|
||||
return True
|
||||
try:
|
||||
return f'{locale.getdefaultlocale()[0]}'[:3] == 'zh_'
|
||||
except (KeyError, Exception):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def wildcard_models(model):
|
||||
if not model:
|
||||
return []
|
||||
if ':' in model:
|
||||
return [model]
|
||||
wil = re.sub(r'\.[^.]+$', '.*', model)
|
||||
return [
|
||||
model,
|
||||
wil,
|
||||
re.sub(r'^[^.]+\.', '*.', wil),
|
||||
'*',
|
||||
]
|
||||
|
||||
|
||||
def convert_globs_to_pattern(globs: list[str] | None):
|
||||
"""Convert a list of globs to a re pattern list."""
|
||||
if not globs:
|
||||
return None
|
||||
translated_patterns = [
|
||||
pattern for glob in set(globs) if (pattern := fnmatch.translate(str(glob)))
|
||||
]
|
||||
if not translated_patterns:
|
||||
return None
|
||||
inner = '|'.join(translated_patterns)
|
||||
combined = f'(?:{inner})'
|
||||
return re.compile(combined)
|
||||
|
||||
|
||||
def get_translation(key, keys=None):
|
||||
dic = get_translations(*(keys or []))
|
||||
val = dic.get(key, key)
|
||||
if isinstance(val, str):
|
||||
return val
|
||||
return key
|
||||
|
||||
|
||||
def get_translations(*keys):
|
||||
dic = {
|
||||
**TRANSLATION_LANGUAGES,
|
||||
**(TRANSLATION_LANGUAGES.get('_globals', {})),
|
||||
}
|
||||
for k in keys:
|
||||
tls = TRANSLATION_LANGUAGES.get(k)
|
||||
if not isinstance(tls, dict):
|
||||
continue
|
||||
dic.update(tls)
|
||||
return dic
|
||||
|
||||
|
||||
def get_translation_langs(hass: HomeAssistant, langs=None):
|
||||
lang = hass.config.language
|
||||
if not langs:
|
||||
return [lang]
|
||||
if 'en' not in langs:
|
||||
langs.append('en')
|
||||
return language_util.matches(lang, langs)
|
||||
|
||||
|
||||
class DeviceException(Exception):
|
||||
"""Exception wrapping any communication errors with the device."""
|
||||
|
||||
|
||||
def is_offline_exception(exc):
|
||||
err = f'{exc}'
|
||||
ret = 'Unable to discover the device' in err
|
||||
if not ret:
|
||||
ret = 'No response from the device' in err
|
||||
if not ret:
|
||||
ret = 'OSError: [Errno 64] Host is down' in err
|
||||
if not ret:
|
||||
ret = 'OSError: [Errno 65] No route to host' in err
|
||||
return ret
|
||||
|
||||
|
||||
def update_attrs_with_suffix(attrs, new_dict):
|
||||
updated_attrs = {}
|
||||
for key, value in new_dict.items():
|
||||
if key in attrs:
|
||||
suffix = 2
|
||||
while f"{key}_{suffix}" in attrs:
|
||||
suffix += 1
|
||||
updated_key = f"{key}_{suffix}"
|
||||
else:
|
||||
updated_key = key
|
||||
|
||||
updated_attrs[updated_key] = value
|
||||
attrs.update(updated_attrs)
|
||||
|
||||
|
||||
def logger_filter(record):
|
||||
def sub(msg):
|
||||
return re.sub(r'[\w/+-]{30,}', '***', msg)
|
||||
if isinstance(record, str):
|
||||
return sub(record)
|
||||
if isinstance(record, logging.LogRecord):
|
||||
record.msg = sub(record.getMessage())
|
||||
return True
|
||||
return record
|
||||
|
||||
|
||||
def int_to_rgb(x: int) -> Tuple[int, int, int]:
|
||||
"""Return a RGB tuple from integer."""
|
||||
red = (x >> 16) & 0xFF
|
||||
green = (x >> 8) & 0xFF
|
||||
blue = x & 0xFF
|
||||
return red, green, blue
|
||||
|
||||
|
||||
def rgb_to_int(x: Tuple[int, int, int]) -> int:
|
||||
"""Return an integer from RGB tuple."""
|
||||
return int(x[0] << 16 | x[1] << 8 | x[2])
|
||||
|
||||
|
||||
def aiohttp_retry(
|
||||
max_retries: int = 3,
|
||||
backoff_factor: float = 2.0,
|
||||
retry_on_status: Optional[Set[int]] = None,
|
||||
exceptions: Tuple[Type[BaseException], ...] = (aiohttp.ClientError, asyncio.TimeoutError),
|
||||
logger=None,
|
||||
):
|
||||
if retry_on_status is None:
|
||||
retry_on_status = {500, 502, 503, 504}
|
||||
|
||||
def decorator(func: Callable):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
last_exception = None
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except exceptions as exc:
|
||||
if logger:
|
||||
logger.warning(
|
||||
"Error while executing request from %s, attempt: %s",
|
||||
func.__name__, attempt, exc_info=True,
|
||||
)
|
||||
last_exception = exc
|
||||
if attempt == max_retries:
|
||||
break
|
||||
is_retryable_status = isinstance(exc, aiohttp.ClientConnectorDNSError)
|
||||
if isinstance(exc, aiohttp.ClientResponseError) and exc.status in retry_on_status:
|
||||
is_retryable_status = True
|
||||
if not is_retryable_status:
|
||||
break
|
||||
delay = backoff_factor * (2 ** attempt)
|
||||
await asyncio.sleep(delay)
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
async def async_analytics_track_event(hass: HomeAssistant, event, action, label, value=0, **kwargs):
|
||||
pms = {
|
||||
'model': label,
|
||||
'event': event,
|
||||
'action': action,
|
||||
'label': label,
|
||||
'value': value,
|
||||
'tz': hass.config.time_zone,
|
||||
'ver': await async_get_manifest(hass, 'version', ''),
|
||||
**kwargs,
|
||||
}
|
||||
url = 'https://hacc.miot-spec.com/api/track'
|
||||
try:
|
||||
session = async_get_clientsession(hass)
|
||||
return await session.post(url, data=pms, timeout=3)
|
||||
except (Exception, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
class RC4:
|
||||
_idx = 0
|
||||
_jdx = 0
|
||||
_ksa: list
|
||||
|
||||
def __init__(self, pwd):
|
||||
self.init_key(pwd)
|
||||
|
||||
def init_key(self, pwd):
|
||||
cnt = len(pwd)
|
||||
ksa = list(range(256))
|
||||
j = 0
|
||||
for i in range(256):
|
||||
j = (j + ksa[i] + pwd[i % cnt]) & 255
|
||||
ksa[i], ksa[j] = ksa[j], ksa[i]
|
||||
self._ksa = ksa
|
||||
self._idx = 0
|
||||
self._jdx = 0
|
||||
return self
|
||||
|
||||
def crypt(self, data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
ksa = self._ksa
|
||||
i = self._idx
|
||||
j = self._jdx
|
||||
out = []
|
||||
for byt in data:
|
||||
i = (i + 1) & 255
|
||||
j = (j + ksa[i]) & 255
|
||||
ksa[i], ksa[j] = ksa[j], ksa[i]
|
||||
out.append(byt ^ ksa[(ksa[i] + ksa[j]) & 255])
|
||||
self._idx = i
|
||||
self._jdx = j
|
||||
self._ksa = ksa
|
||||
return bytearray(out)
|
||||
|
||||
def init1024(self):
|
||||
self.crypt(bytes(1024))
|
||||
return self
|
||||
@@ -0,0 +1,954 @@
|
||||
import logging
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import string
|
||||
import random
|
||||
import base64
|
||||
import locale
|
||||
import hashlib
|
||||
import micloud
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
from urllib import parse
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.components import persistent_notification
|
||||
|
||||
from .const import DOMAIN, CONF_XIAOMI_CLOUD
|
||||
from .utils import RC4, aiohttp_retry, local_zone, logger_filter
|
||||
from micloud import miutils
|
||||
from micloud.micloudexception import MiCloudException
|
||||
|
||||
try:
|
||||
from micloud.micloudexception import MiCloudAccessDenied
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
class MiCloudAccessDenied(MiCloudException):
|
||||
""" micloud==0.4 """
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER.addFilter(logger_filter)
|
||||
|
||||
ACCOUNT_BASE = 'https://account.xiaomi.com'
|
||||
UA = "Android-7.1.1-1.0.0-ONEPLUS A3010-136-%s APP/xiaomi.smarthome APPV/62830"
|
||||
|
||||
|
||||
class MiotCloud(micloud.MiCloud):
|
||||
user_id = None
|
||||
cuser_id = None
|
||||
ssecurity = None
|
||||
pass_token = None
|
||||
service_token = None
|
||||
failed_logins = 0
|
||||
session = None
|
||||
async_session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
def __init__(self, hass, username, password, country=None, sid=None):
|
||||
try:
|
||||
super().__init__(username, password)
|
||||
timezone = datetime.now(local_zone(hass)).strftime('%z')
|
||||
self.timezone = 'GMT{0}:{1}'.format(timezone[:-2], timezone[-2:])
|
||||
self.locale = locale.getlocale()[0]
|
||||
except Exception:
|
||||
self.timezone = 'GMT+08:00'
|
||||
self.locale = 'zh_CN'
|
||||
|
||||
self.hass = hass
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.default_server = country or 'cn'
|
||||
self.sid = sid or 'xiaomiio'
|
||||
self.agent_id = self.get_random_string(16)
|
||||
self.client_id = self.agent_id
|
||||
self.useragent = UA % self.client_id
|
||||
self.http_timeout = int(hass.data[DOMAIN].get('config', {}).get('http_timeout') or 10)
|
||||
self.login_times = 0
|
||||
self.cookies = {}
|
||||
self.attrs = {}
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
uid = self.user_id or self.username
|
||||
return f'{uid}-{self.default_server}-{self.sid}'
|
||||
|
||||
async def async_get_properties_for_mapping(self, did, mapping: dict):
|
||||
pms = []
|
||||
rmp = {}
|
||||
for k, v in mapping.items():
|
||||
if not isinstance(v, dict):
|
||||
continue
|
||||
s = v.get('siid')
|
||||
p = v.get('piid')
|
||||
pms.append({'did': str(did), 'siid': s, 'piid': p})
|
||||
rmp[f'prop.{s}.{p}'] = k
|
||||
rls = await self.async_get_props(pms)
|
||||
if not rls:
|
||||
return None
|
||||
dls = []
|
||||
for v in rls:
|
||||
s = v.get('siid')
|
||||
p = v.get('piid')
|
||||
k = rmp.get(f'prop.{s}.{p}')
|
||||
if not k:
|
||||
continue
|
||||
v['prop'] = k
|
||||
dls.append(v)
|
||||
return dls
|
||||
|
||||
async def async_get_props(self, params=None):
|
||||
return await self.async_request_miot_spec('prop/get', params)
|
||||
|
||||
@aiohttp_retry(3, logger=_LOGGER)
|
||||
async def async_set_props(self, params=None):
|
||||
return await self.async_request_miot_spec('prop/set', params, timeout=5, raise_timeout=True)
|
||||
|
||||
async def async_do_action(self, params=None):
|
||||
return await self.async_request_miot_spec('action', params)
|
||||
|
||||
async def async_request_miot_spec(self, api, params=None, **kwargs):
|
||||
resp = await self.async_request_api('miotspec/' + api, {
|
||||
'params': params or [],
|
||||
}, **kwargs) or {}
|
||||
result = resp.get('result')
|
||||
if not result and self.is_token_expired(resp):
|
||||
now = int(time.time())
|
||||
tim = self.attrs.get('last_relogin_time', 0)
|
||||
if now - tim > 600:
|
||||
self.attrs['last_relogin_time'] = now
|
||||
if await self.async_check_auth(notify=True):
|
||||
return await self.async_request_miot_spec(api, params, **kwargs)
|
||||
raise MiCloudException(json.dumps(resp, ensure_ascii=False))
|
||||
if not result and resp.get('code'):
|
||||
raise MiCloudException(json.dumps(resp, ensure_ascii=False))
|
||||
return result
|
||||
|
||||
async def async_get_user_device_data(self, did, key, typ='prop', raw=False, **kwargs):
|
||||
now = int(time.time())
|
||||
timeout = kwargs.pop('timeout', self.http_timeout)
|
||||
params = {
|
||||
'did': did,
|
||||
'key': key,
|
||||
'type': typ,
|
||||
'time_start': now - 86400 * 7,
|
||||
'time_end': now + 60,
|
||||
'limit': 5,
|
||||
**kwargs,
|
||||
}
|
||||
rdt = await self.async_request_api('user/get_user_device_data', params, timeout=timeout) or {}
|
||||
return rdt if raw else rdt.get('result')
|
||||
|
||||
async def async_get_last_device_data(self, did, key, typ='prop', **kwargs):
|
||||
kwargs['raw'] = False
|
||||
kwargs['limit'] = 1
|
||||
rls = await self.async_get_user_device_data(did, key, typ, **kwargs) or [None]
|
||||
rdt = rls.pop(0) or {}
|
||||
if kwargs.get('not_value'):
|
||||
return rdt
|
||||
val = rdt.get('value')
|
||||
if val is None:
|
||||
return None
|
||||
try:
|
||||
vls = json.loads(val)
|
||||
except (TypeError, ValueError):
|
||||
vls = [val]
|
||||
return vls.pop(0)
|
||||
|
||||
async def async_check_auth(self, notify=False):
|
||||
if self.service_token:
|
||||
api = 'v2/message/v2/check_new_msg'
|
||||
dat = {
|
||||
'begin_at': int(time.time()) - 60,
|
||||
}
|
||||
try:
|
||||
rdt = await self.async_request_api(api, dat, method='POST') or {}
|
||||
if not self.is_token_expired(rdt):
|
||||
return True
|
||||
except requests.exceptions.ConnectionError:
|
||||
return None
|
||||
except requests.exceptions.Timeout:
|
||||
return None
|
||||
# auth err
|
||||
_LOGGER.info('Xiaomi auth failed, try relogin. %s', rdt)
|
||||
nid = f'xiaomi-miot-auth-warning-{self.user_id}'
|
||||
need_verify = None
|
||||
try:
|
||||
if await self.async_relogin():
|
||||
persistent_notification.dismiss(self.hass, nid)
|
||||
return True
|
||||
except MiCloudNeedVerify as exc:
|
||||
need_verify = exc
|
||||
if notify:
|
||||
lnk = f'/config/integrations/integration/{DOMAIN}'
|
||||
persistent_notification.create(
|
||||
self.hass,
|
||||
f'Xiaomi account: {self.user_id} auth failed, '
|
||||
f'Your Xiaomi account login status has expired, Please login again through [integrated configuration]({lnk}).\n'
|
||||
f'你的小米账号登陆状态已失效,但本次需要手动验证,请通过[集成配置]({lnk})重新登陆。',
|
||||
'Xiaomi Miot Warning',
|
||||
nid,
|
||||
)
|
||||
_LOGGER.error(
|
||||
'Xiaomi account: %s auth failed, Please update option for this integration to refresh token.\n%s',
|
||||
self.user_id,
|
||||
rdt,
|
||||
)
|
||||
elif need_verify:
|
||||
raise need_verify
|
||||
else:
|
||||
_LOGGER.warning('Retry login xiaomi account failed: %s', self.username)
|
||||
return False
|
||||
|
||||
def is_token_expired(self, data: dict):
|
||||
eno = data.get('code', 0)
|
||||
msg = data.get('message', '')
|
||||
if eno in [2, 3]:
|
||||
return True
|
||||
elif 'auth err' in msg:
|
||||
return True
|
||||
elif msg in ['invalid signature', 'SERVICETOKEN_EXPIRED']:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_request_api(self, api, data, method='POST', crypt=True, debug=True, **kwargs):
|
||||
if not self.service_token:
|
||||
await self.async_login()
|
||||
|
||||
params = {}
|
||||
if data is not None:
|
||||
params['data'] = self.json_encode(data)
|
||||
raise_timeout = kwargs.pop('raise_timeout', None)
|
||||
raw = kwargs.pop('raw', self.sid != 'xiaomiio')
|
||||
rsp = None
|
||||
try:
|
||||
if raw:
|
||||
rsp = await self.hass.async_add_executor_job(
|
||||
partial(self.request_raw, api, data, method, **kwargs)
|
||||
)
|
||||
elif crypt:
|
||||
rsp = await self.async_request_rc4_api(api, params, method, **kwargs)
|
||||
else:
|
||||
rsp = await self.hass.async_add_executor_job(
|
||||
partial(self.request, self.get_api_url(api), params, **kwargs)
|
||||
)
|
||||
rdt = json.loads(rsp)
|
||||
if debug:
|
||||
_LOGGER.debug(
|
||||
'Request miot api: %s %s result: %s',
|
||||
api, data, rsp,
|
||||
)
|
||||
self.attrs['timeouts'] = 0
|
||||
except asyncio.TimeoutError as exc:
|
||||
if raise_timeout:
|
||||
raise exc
|
||||
rdt = None
|
||||
self.attrs.setdefault('timeouts', 0)
|
||||
self.attrs['timeouts'] += 1
|
||||
if 5 < self.attrs['timeouts'] <= 10:
|
||||
_LOGGER.error('Request xiaomi api: %s %s timeout, exception: %s', api, data or {}, exc)
|
||||
elif self.attrs['timeouts'] <= 5:
|
||||
_LOGGER.warning('Request xiaomi api: %s timeout (%s times)', api, self.attrs['timeouts'])
|
||||
except asyncio.exceptions.CancelledError as exc:
|
||||
rdt = None
|
||||
_LOGGER.warning('Request xiaomi api: %s was cancelled, likely due to timeout: %s', api, exc)
|
||||
except (TypeError, ValueError):
|
||||
rdt = None
|
||||
code = rdt.get('code') if rdt else None
|
||||
if code == 3:
|
||||
self._logout()
|
||||
_LOGGER.warning('Unauthorized while request to %s, response: %s, logged out.', api, rsp)
|
||||
elif code or not rdt:
|
||||
fun = _LOGGER.info if rdt else _LOGGER.warning
|
||||
fun('Request xiaomi api: %s %s failed, response: %s', api, data, rsp)
|
||||
return rdt
|
||||
|
||||
async def async_get_device(self, mac=None, host=None):
|
||||
dvs = await self.async_get_devices() or []
|
||||
for d in dvs:
|
||||
if not isinstance(d, dict):
|
||||
continue
|
||||
if mac and mac == d.get('mac'):
|
||||
return d
|
||||
if host and host == d.get('localip'):
|
||||
return d
|
||||
return None
|
||||
|
||||
async def get_device_list(self):
|
||||
rdt = await self.async_request_api('home/device_list', {
|
||||
'getVirtualModel': True,
|
||||
'getHuamiDevices': 1,
|
||||
'get_split_device': False,
|
||||
'support_smart_home': True,
|
||||
}, debug=False, timeout=60) or {}
|
||||
result = rdt.get('result')
|
||||
if result:
|
||||
return result['list']
|
||||
_LOGGER.warning('Got xiaomi devices for %s failed: %s', self.username, rdt)
|
||||
return None
|
||||
|
||||
async def get_all_devices(self, homes=None):
|
||||
devices = {
|
||||
d['did']: d
|
||||
for d in await self.get_device_list() or []
|
||||
}
|
||||
if not isinstance(homes, list):
|
||||
return await self.get_device_list() or []
|
||||
for home in homes:
|
||||
hid = int(home.get('id', 0))
|
||||
uid = int(home.get('uid', 0))
|
||||
start_did = ''
|
||||
has_more = True
|
||||
while has_more:
|
||||
rdt = await self.async_request_api('v2/home/home_device_list', {
|
||||
'home_owner': uid,
|
||||
'home_id': hid,
|
||||
'limit': 300,
|
||||
'start_did': start_did,
|
||||
'get_split_device': False,
|
||||
'support_smart_home': True,
|
||||
'get_cariot_device': True,
|
||||
'get_third_device': True,
|
||||
}, debug=False, timeout=20) or {}
|
||||
result = rdt.get('result') or {}
|
||||
if not result:
|
||||
_LOGGER.warning('Got xiaomi devices for %s failed: %s', self.username, rdt)
|
||||
for d in result.get('device_info') or []:
|
||||
did = d.get('did')
|
||||
devices.setdefault(did, {}).update(d)
|
||||
start_did = result.get('max_did') or ''
|
||||
has_more = result.get('has_more') and start_did
|
||||
return list(devices.values())
|
||||
|
||||
async def get_home_devices(self):
|
||||
rdt = await self.async_request_api('v2/homeroom/gethome_merged', {
|
||||
'fg': True,
|
||||
'fetch_share': True,
|
||||
'fetch_share_dev': True,
|
||||
'fetch_cariot': True,
|
||||
'limit': 300,
|
||||
'app_ver': 7,
|
||||
'plat_form': 0,
|
||||
}, debug=False, timeout=60) or {}
|
||||
result = rdt.get('result') or {}
|
||||
if not result:
|
||||
_LOGGER.warning('Got xiaomi home devices for %s failed: %s', self.username, rdt)
|
||||
devices = result.setdefault('devices', {})
|
||||
for h in result.get('homelist', []):
|
||||
for r in h.get('roomlist', []):
|
||||
for did in r.get('dids', []):
|
||||
devices[did] = {
|
||||
'home_id': h.get('id'),
|
||||
'room_id': r.get('id'),
|
||||
'home_name': h.get('name'),
|
||||
'room_name': r.get('name'),
|
||||
}
|
||||
return result
|
||||
|
||||
async def async_get_devices(self, renew=False, return_all=False):
|
||||
if not self.user_id:
|
||||
return None
|
||||
fnm = f'xiaomi_miot/devices-{self.user_id}-{self.default_server}.json'
|
||||
store = Store(self.hass, 1, fnm)
|
||||
now = time.time()
|
||||
cds = []
|
||||
dvs = []
|
||||
try:
|
||||
dat = await store.async_load() or {}
|
||||
except ValueError:
|
||||
await store.async_remove()
|
||||
dat = {}
|
||||
if isinstance(dat, dict):
|
||||
cds = dat.get('devices') or []
|
||||
if not renew and dat.get('update_time', 0) > (now - 86400):
|
||||
dvs = cds
|
||||
if not dvs:
|
||||
try:
|
||||
hls = await self.get_home_devices()
|
||||
dvs = await self.get_all_devices(hls.get('homelist', []))
|
||||
if dvs:
|
||||
if hls:
|
||||
hds = hls.get('devices') or {}
|
||||
dvs = [
|
||||
{**d, **(hds.get(d.get('did')) or {})}
|
||||
for d in dvs
|
||||
]
|
||||
dat = {
|
||||
'update_time': now,
|
||||
'devices': dvs,
|
||||
'homes': hls.get('homelist', []),
|
||||
}
|
||||
await store.async_save(dat)
|
||||
_LOGGER.info('Got %s devices from xiaomi cloud', len(dvs))
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
if not cds:
|
||||
raise exc
|
||||
dvs = cds
|
||||
_LOGGER.warning('Get xiaomi devices filed: %s, use cached %s devices.', exc, len(cds))
|
||||
if return_all:
|
||||
return dat
|
||||
return dvs
|
||||
|
||||
async def async_renew_devices(self):
|
||||
return await self.async_get_devices(renew=True)
|
||||
|
||||
async def async_get_devices_by_key(self, key, renew=False, filters=None):
|
||||
dat = {}
|
||||
if filters is None:
|
||||
filters = {}
|
||||
fls = ['ssid', 'bssid', 'home_id', 'model', 'did']
|
||||
dvs = await self.async_get_devices(renew=renew) or []
|
||||
for d in dvs:
|
||||
if not isinstance(d, dict):
|
||||
continue
|
||||
if self.is_hide(d):
|
||||
continue
|
||||
if not d.get('mac'):
|
||||
d['mac'] = d.get('did')
|
||||
k = d.get(key)
|
||||
for f in fls:
|
||||
ft = filters.get(f'filter_{f}')
|
||||
if not ft:
|
||||
continue
|
||||
ex = ft != 'include'
|
||||
fl = filters.get(f'{f}_list') or {}
|
||||
fv = d.get(f)
|
||||
if ex:
|
||||
ok = fv not in fl
|
||||
else:
|
||||
ok = fv in fl
|
||||
if not ok:
|
||||
k = None
|
||||
if k:
|
||||
dat[k] = d
|
||||
return dat
|
||||
|
||||
async def async_get_homerooms(self, renew=False):
|
||||
dat = await self.async_get_devices(renew=renew, return_all=True) or {}
|
||||
return dat.get('homes') or []
|
||||
|
||||
async def async_get_beaconkey(self, did):
|
||||
dat = {'did': did, 'pdid': 1}
|
||||
rdt = await self.async_request_api('v2/device/blt_get_beaconkey', dat) or {}
|
||||
return rdt.get('result', rdt)
|
||||
|
||||
@staticmethod
|
||||
def is_hide(d):
|
||||
did = d.get('did', '')
|
||||
pid = d.get('pid', '')
|
||||
if pid == '21':
|
||||
prt = d.get('parent_id')
|
||||
if prt and prt in did:
|
||||
# issues/263
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_login(self, login_data=None):
|
||||
if self.login_times > 5:
|
||||
await self.async_stored_auth(remove=True)
|
||||
if self.login_times > 10:
|
||||
raise MiCloudException(
|
||||
'Too many failures when login to Xiaomi, '
|
||||
'please reload/config xiaomi_miot component.'
|
||||
)
|
||||
self.login_times += 1
|
||||
ret = await self.hass.async_add_executor_job(self._login_request, login_data)
|
||||
if ret:
|
||||
self.hass.data[DOMAIN]['sessions'][self.unique_id] = self
|
||||
await self.async_stored_auth(save=True)
|
||||
self.login_times = 0
|
||||
return ret
|
||||
|
||||
async def async_relogin(self):
|
||||
self._logout()
|
||||
return await self.async_login()
|
||||
|
||||
def _logout(self):
|
||||
self.service_token = None
|
||||
self.async_session = None
|
||||
|
||||
def _login_request(self, login_data=None):
|
||||
self._init_session(True)
|
||||
location = ''
|
||||
auth = self.attrs.pop('login_data', {})
|
||||
if not login_data:
|
||||
pass
|
||||
elif ticket := login_data.get('verify_ticket'):
|
||||
resp = self.verify_ticket(ticket)
|
||||
location = resp.get('location', '')
|
||||
if location:
|
||||
self.account_get(location, allow_redirects=True)
|
||||
auth = self._login_step1()
|
||||
location = auth.get('location', '')
|
||||
elif auth:
|
||||
auth.update(login_data)
|
||||
else:
|
||||
auth = self._login_step1()
|
||||
if not location:
|
||||
location = self._login_step2(**auth)
|
||||
response = self._login_step3(location)
|
||||
http_code = response.status_code
|
||||
if http_code == 200:
|
||||
return True
|
||||
elif http_code == 403:
|
||||
raise MiCloudAccessDenied(f'Login to xiaomi error: {response.text} ({http_code})')
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Xiaomi login request returned status %s, reason: %s, content: %s',
|
||||
http_code, response.reason, response.text,
|
||||
)
|
||||
raise MiCloudException(f'Login to xiaomi error: {response.text} ({http_code})')
|
||||
|
||||
def _login_step1(self):
|
||||
self.cookies.update({'sdkVersion': '3.8.6', 'deviceId': self.client_id})
|
||||
try:
|
||||
auth = self.account_get(
|
||||
'/pass/serviceLogin',
|
||||
params={'sid': self.sid, '_json': 'true'},
|
||||
headers={'User-Agent': self.useragent},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise MiCloudException(f'Error getting xiaomi login sign. Cannot parse response. {exc}')
|
||||
if auth.get('code') == 0:
|
||||
self.user_id = auth.get('userId', self.user_id)
|
||||
self.cuser_id = auth.get('cUserId', self.cuser_id)
|
||||
self.ssecurity = auth.get('ssecurity', self.ssecurity)
|
||||
self.pass_token = auth.get('passToken', self.pass_token)
|
||||
self.async_session = None
|
||||
return auth
|
||||
|
||||
def _login_step2(self, captcha=None, **kwargs):
|
||||
url = '/pass/serviceLoginAuth2'
|
||||
post = {
|
||||
'user': self.username,
|
||||
'hash': hashlib.md5(self.password.encode()).hexdigest().upper(),
|
||||
'callback': kwargs.get('callback') or '',
|
||||
'sid': kwargs.get('sid') or self.sid,
|
||||
'qs': kwargs.get('qs') or '',
|
||||
'_sign': kwargs.get('_sign') or '',
|
||||
}
|
||||
params = {'_json': 'true'}
|
||||
cookies = {}
|
||||
if captcha:
|
||||
post['captCode'] = captcha
|
||||
params['_dc'] = int(time.time() * 1000)
|
||||
cookies['ick'] = self.attrs.pop('captchaIck', '')
|
||||
response = self.account_post(url, data=post, params=params, cookies=cookies, response=True)
|
||||
auth = self.json_decode(response.text) or {}
|
||||
code = auth.get('code')
|
||||
# 20003 InvalidUserNameException
|
||||
# 22009 PackageNameDeniedException
|
||||
# 70002 InvalidCredentialException
|
||||
# 70016 InvalidCredentialException with captchaUrl / Password error
|
||||
# 81003 NeedVerificationException
|
||||
# 87001 InvalidResponseException captCode error
|
||||
# other NeedCaptchaException
|
||||
location = auth.get('location')
|
||||
if not location:
|
||||
if ntf := auth.get('notificationUrl'):
|
||||
if ntf[:4] != 'http':
|
||||
ntf = f'{ACCOUNT_BASE}{ntf}'
|
||||
self.attrs['verify_url'] = ntf
|
||||
raise MiCloudNeedVerify('need_verify').with_url(ntf)
|
||||
if cap := auth.get('captchaUrl'):
|
||||
if cap[:4] != 'http':
|
||||
cap = f'{ACCOUNT_BASE}{cap}'
|
||||
if self._get_captcha(cap):
|
||||
self.attrs['login_data'] = kwargs
|
||||
_LOGGER.error(
|
||||
'Xiaomi serviceLoginAuth2: %s' %
|
||||
[url, self.login_times, {**post, 'hash': '*'}, cookies, response.text],
|
||||
)
|
||||
raise MiCloudAccessDenied(f'Login to xiaomi error: {response.text}')
|
||||
self.user_id = str(auth.get('userId', ''))
|
||||
self.cuser_id = auth.get('cUserId')
|
||||
self.ssecurity = auth.get('ssecurity')
|
||||
self.pass_token = auth.get('passToken')
|
||||
if self.sid != 'xiaomiio':
|
||||
sign = f'nonce={auth.get("nonce")}&{auth.get("ssecurity")}'
|
||||
sign = hashlib.sha1(sign.encode()).digest()
|
||||
sign = base64.b64encode(sign).decode()
|
||||
location += '&clientSign=' + parse.quote(sign)
|
||||
_LOGGER.info('Xiaomi serviceLoginAuth2: %s', [auth, self.cookies])
|
||||
return location
|
||||
|
||||
def _login_step3(self, location):
|
||||
self.session.headers.update({'content-type': 'application/x-www-form-urlencoded'})
|
||||
response = self.account_get(location, response=True)
|
||||
cookies = response.cookies
|
||||
service_token = cookies.get('serviceToken')
|
||||
if service_token:
|
||||
self.service_token = service_token
|
||||
self.user_id = cookies.get('userId', self.user_id)
|
||||
self.cuser_id = cookies.get('cUserId', self.cuser_id)
|
||||
self.async_session = None
|
||||
else:
|
||||
err = {
|
||||
'location': location,
|
||||
'status_code': response.status_code,
|
||||
'cookies': cookies.get_dict(),
|
||||
'response': response.text,
|
||||
}
|
||||
raise MiCloudAccessDenied(f'Login to xiaomi error: {err}')
|
||||
return response
|
||||
|
||||
def _get_captcha(self, url):
|
||||
response = self.session.get(url)
|
||||
if ick := response.cookies.get('ick'):
|
||||
self.attrs['captchaIck'] = ick
|
||||
self.attrs['captchaImg'] = base64.b64encode(response.content).decode()
|
||||
return ick
|
||||
|
||||
def check_identity_list(self, url, path='fe/service/identity/authStart'):
|
||||
if path not in url:
|
||||
return None
|
||||
resp = self.account_get(url.replace(path, 'identity/list'), response=True)
|
||||
identity_session = resp.cookies.get('identity_session')
|
||||
if not identity_session:
|
||||
return False
|
||||
data = self.json_decode(resp.text) or {}
|
||||
flag = data.get('flag', 4)
|
||||
options = data.get('options', [flag])
|
||||
return options
|
||||
|
||||
def verify_ticket(self, ticket):
|
||||
url = self.attrs.get('verify_url')
|
||||
if not url:
|
||||
return {}
|
||||
options = self.check_identity_list(url) or []
|
||||
for flag in options:
|
||||
api = {
|
||||
4: '/identity/auth/verifyPhone',
|
||||
8: '/identity/auth/verifyEmail',
|
||||
}.get(flag)
|
||||
if not api:
|
||||
continue
|
||||
data = self.account_post(
|
||||
api,
|
||||
params={
|
||||
'_dc': int(time.time() * 1000),
|
||||
},
|
||||
data={
|
||||
'_flag': flag,
|
||||
'ticket': ticket,
|
||||
'trust': 'true',
|
||||
'_json': 'true',
|
||||
},
|
||||
cookies={
|
||||
'identity_session': self.attrs.get('identity_session'),
|
||||
},
|
||||
)
|
||||
if data.get('code') == 0:
|
||||
self.attrs.pop('identity_session', None)
|
||||
return data
|
||||
return {}
|
||||
|
||||
def account_get(self, url, method='GET', **kwargs):
|
||||
return self.account_post(url, method, **kwargs)
|
||||
|
||||
def account_post(self, url, method='POST', **kwargs):
|
||||
if url[:4] != 'http':
|
||||
url = f'{ACCOUNT_BASE}{url}'
|
||||
kwargs['cookies'] = {
|
||||
**self.cookies,
|
||||
**kwargs.get('cookies', {}),
|
||||
}
|
||||
kwargs.setdefault('headers', {'User-Agent': self.useragent})
|
||||
response = kwargs.pop('response', None)
|
||||
resp = self.session.request(method, url, **kwargs)
|
||||
try:
|
||||
data = self.json_decode(resp.text) or {}
|
||||
except Exception:
|
||||
data = {
|
||||
'code': resp.status_code,
|
||||
'response': resp.text,
|
||||
}
|
||||
cookies = resp.cookies.get_dict()
|
||||
self.cookies.update(cookies)
|
||||
log = _LOGGER.warning if data.get('code') else _LOGGER.info
|
||||
log('Account request: %s' % [url, kwargs, resp.text, cookies])
|
||||
if response:
|
||||
return resp
|
||||
return data
|
||||
|
||||
def json_decode(self, text):
|
||||
return json.loads(text.replace('&&&START&&&', ''))
|
||||
|
||||
def to_config(self):
|
||||
return {
|
||||
CONF_USERNAME: self.username,
|
||||
CONF_PASSWORD: self.password,
|
||||
'server_country': self.default_server,
|
||||
'user_id': self.user_id,
|
||||
'service_token': self.service_token,
|
||||
'ssecurity': self.ssecurity,
|
||||
'sid': self.sid,
|
||||
'device_id': self.client_id,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def from_token(hass, config: dict, login=None):
|
||||
mic = await hass.async_add_executor_job(
|
||||
MiotCloud,
|
||||
hass,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
config.get('server_country'),
|
||||
config.get('sid'),
|
||||
)
|
||||
mic.user_id = str(config.get('user_id') or '')
|
||||
if a := hass.data[DOMAIN].get('sessions', {}).get(mic.unique_id):
|
||||
mic = a
|
||||
mic.merger_config(config)
|
||||
if not mic.service_token:
|
||||
sdt = await mic.async_stored_auth(save=False)
|
||||
config.update(sdt)
|
||||
mic.service_token = config.get('service_token')
|
||||
mic.ssecurity = config.get('ssecurity')
|
||||
did = config.get('device_id') or ''
|
||||
if did and len(did) <= 32:
|
||||
mic.client_id = did
|
||||
mic.useragent = UA % did
|
||||
if login is None:
|
||||
if not mic.service_token:
|
||||
login = True
|
||||
if login:
|
||||
await mic.async_login()
|
||||
else:
|
||||
hass.data[DOMAIN]['sessions'][mic.unique_id] = mic
|
||||
return mic
|
||||
|
||||
def merger_config(self, config: dict, changed=None):
|
||||
if self.username != config.get(CONF_USERNAME):
|
||||
self.username = config.get(CONF_USERNAME)
|
||||
changed = True
|
||||
if self.password != config.get(CONF_PASSWORD):
|
||||
self.password = config.get(CONF_PASSWORD)
|
||||
changed = True
|
||||
if changed:
|
||||
self.async_session = None
|
||||
self.service_token = None
|
||||
self.ssecurity = None
|
||||
self.cookies = {}
|
||||
self.attrs = {}
|
||||
return self
|
||||
|
||||
async def async_change_sid(self, sid: str, login=None):
|
||||
config = {
|
||||
**self.to_config(),
|
||||
'sid': sid,
|
||||
'service_token': None,
|
||||
'ssecurity': None,
|
||||
}
|
||||
mic = await self.from_token(self.hass, config, login)
|
||||
return mic
|
||||
|
||||
async def async_stored_auth(self, uid=None, save=False, remove=False):
|
||||
if not uid:
|
||||
uid = self.user_id or self.username
|
||||
fnm = f'xiaomi_miot/auth-{uid}-{self.default_server}.json'
|
||||
if self.sid != 'xiaomiio':
|
||||
fnm = fnm.replace('.json', f'-{self.sid}.json')
|
||||
store = Store(self.hass, 1, fnm)
|
||||
if remove:
|
||||
await store.async_remove()
|
||||
try:
|
||||
old = await store.async_load() or {}
|
||||
except ValueError:
|
||||
if not remove:
|
||||
await store.async_remove()
|
||||
old = {}
|
||||
if save:
|
||||
cfg = self.to_config()
|
||||
cfg.pop(CONF_PASSWORD, None)
|
||||
if cfg.get('service_token') == old.get('service_token'):
|
||||
cfg['update_at'] = old.get('update_at')
|
||||
else:
|
||||
cfg['update_at'] = f'{datetime.fromtimestamp(int(time.time()))}'
|
||||
await store.async_save(cfg)
|
||||
return cfg
|
||||
return old
|
||||
|
||||
def api_session(self, **kwargs):
|
||||
if not self.service_token or not self.user_id:
|
||||
raise MiCloudException('Cannot execute request. service token or userId missing. Make sure to login.')
|
||||
|
||||
if kwargs.get('async'):
|
||||
session = self.async_session
|
||||
if not session or session.closed:
|
||||
session = async_create_clientsession(
|
||||
self.hass,
|
||||
headers=self.api_headers(),
|
||||
cookies=self.api_cookies(),
|
||||
)
|
||||
self.async_session = session
|
||||
else:
|
||||
session = requests.Session()
|
||||
session.headers.update(self.api_headers())
|
||||
session.cookies.update(self.api_cookies())
|
||||
return session
|
||||
|
||||
def api_headers(self):
|
||||
return {
|
||||
'X-XIAOMI-PROTOCAL-FLAG-CLI': 'PROTOCAL-HTTP2',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': self.useragent,
|
||||
}
|
||||
|
||||
def api_cookies(self):
|
||||
return {
|
||||
'userId': str(self.user_id),
|
||||
'yetAnotherServiceToken': self.service_token,
|
||||
'serviceToken': self.service_token,
|
||||
'locale': str(self.locale),
|
||||
'timezone': str(self.timezone),
|
||||
'is_daylight': str(time.daylight),
|
||||
'dst_offset': str(time.localtime().tm_isdst * 60 * 60 * 1000),
|
||||
'channel': 'MI_APP_STORE',
|
||||
}
|
||||
|
||||
def request(self, url, params, **kwargs):
|
||||
self.session = self.api_session()
|
||||
timeout = kwargs.get('timeout', self.http_timeout)
|
||||
try:
|
||||
nonce = miutils.gen_nonce()
|
||||
signed_nonce = miutils.signed_nonce(self.ssecurity, nonce)
|
||||
signature = miutils.gen_signature(url.replace('/app/', '/'), signed_nonce, nonce, params)
|
||||
post_data = {
|
||||
'signature': signature,
|
||||
'_nonce': nonce,
|
||||
'data': params['data'],
|
||||
}
|
||||
response = self.session.post(url, data=post_data, timeout=timeout)
|
||||
return response.text
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
_LOGGER.error('Error while executing request to %s: %s', url, exc)
|
||||
except MiCloudException as exc:
|
||||
_LOGGER.error('Error while decrypting response of request to %s: %s', url, exc)
|
||||
|
||||
async def async_request_rc4_api(self, api, params: dict, method='POST', **kwargs):
|
||||
url = self.get_api_url(api)
|
||||
session = self.api_session(**{'async': True})
|
||||
timeout = aiohttp.ClientTimeout(total=kwargs.get('timeout', self.http_timeout))
|
||||
headers = {
|
||||
'MIOT-ENCRYPT-ALGORITHM': 'ENCRYPT-RC4',
|
||||
'Accept-Encoding': 'identity',
|
||||
}
|
||||
params = self.rc4_params(method, url, params)
|
||||
if method == 'GET':
|
||||
response = await session.get(url, params=params, timeout=timeout, headers=headers)
|
||||
else:
|
||||
response = await session.post(url, data=params, timeout=timeout, headers=headers)
|
||||
rsp = await response.text()
|
||||
if not rsp or 'error' in rsp or 'invalid' in rsp:
|
||||
_LOGGER.warning('Error while executing request to %s: %s', url, rsp or response.status)
|
||||
elif 'message' not in rsp:
|
||||
try:
|
||||
signed_nonce = self.signed_nonce(params['_nonce'])
|
||||
rsp = MiotCloud.decrypt_data(signed_nonce, rsp)
|
||||
except ValueError:
|
||||
_LOGGER.warning('Error while decrypting response of request to %s :%s', url, rsp)
|
||||
return rsp
|
||||
|
||||
def request_raw(self, url, data=None, method='GET', **kwargs):
|
||||
self.session = self.api_session()
|
||||
url = self.get_api_url(url)
|
||||
kwargs.setdefault('params' if method == 'GET' else 'data', data)
|
||||
kwargs.setdefault('timeout', self.http_timeout)
|
||||
try:
|
||||
response = self.session.request(method, url, **kwargs)
|
||||
if response.status_code == 401:
|
||||
self._logout()
|
||||
_LOGGER.warning('Unauthorized while executing request to %s, logged out.', url)
|
||||
rsp = response.text
|
||||
if not rsp or 'error' in rsp or 'invalid' in rsp:
|
||||
log = _LOGGER.info if 'remote/ubus' in url else _LOGGER.warning
|
||||
log('Error while executing request to %s: %s', url, rsp or response.status_code)
|
||||
return rsp
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
_LOGGER.warning('Error while executing request to %s: %s', url, exc)
|
||||
return None
|
||||
|
||||
def get_api_by_host(self, host, api=''):
|
||||
srv = self.default_server.lower()
|
||||
if srv and srv != 'cn':
|
||||
host = f'{srv}.{host}'
|
||||
api = str(api).lstrip('/')
|
||||
return f'https://{host}/{api}'
|
||||
|
||||
def get_api_url(self, api):
|
||||
if api[:6] == 'https:' or api[:5] == 'http:':
|
||||
url = api
|
||||
else:
|
||||
api = str(api).lstrip('/')
|
||||
url = self._get_api_url(self.default_server) + '/' + api
|
||||
return url
|
||||
|
||||
def rc4_params(self, method, url, params: dict):
|
||||
nonce = miutils.gen_nonce()
|
||||
signed_nonce = self.signed_nonce(nonce)
|
||||
params['rc4_hash__'] = MiotCloud.sha1_sign(method, url, params, signed_nonce)
|
||||
for k, v in params.items():
|
||||
params[k] = MiotCloud.encrypt_data(signed_nonce, v)
|
||||
params.update({
|
||||
'signature': MiotCloud.sha1_sign(method, url, params, signed_nonce),
|
||||
'ssecurity': self.ssecurity,
|
||||
'_nonce': nonce,
|
||||
})
|
||||
return params
|
||||
|
||||
def signed_nonce(self, nonce):
|
||||
return miutils.signed_nonce(self.ssecurity, nonce)
|
||||
|
||||
@staticmethod
|
||||
def json_encode(data):
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
@staticmethod
|
||||
def sha1_sign(method, url, dat: dict, nonce):
|
||||
path = parse.urlparse(url).path
|
||||
if path[:5] == '/app/':
|
||||
path = path[4:]
|
||||
arr = [str(method).upper(), path]
|
||||
for k, v in dat.items():
|
||||
arr.append(f'{k}={v}')
|
||||
arr.append(nonce)
|
||||
raw = hashlib.sha1('&'.join(arr).encode('utf-8')).digest()
|
||||
return base64.b64encode(raw).decode()
|
||||
|
||||
@staticmethod
|
||||
def encrypt_data(pwd, data):
|
||||
return base64.b64encode(RC4(base64.b64decode(pwd)).init1024().crypt(data)).decode()
|
||||
|
||||
@staticmethod
|
||||
def decrypt_data(pwd, data):
|
||||
return RC4(base64.b64decode(pwd)).init1024().crypt(base64.b64decode(data))
|
||||
|
||||
@staticmethod
|
||||
def all_clouds(hass):
|
||||
cls = {}
|
||||
for k, v in hass.data[DOMAIN].items():
|
||||
if isinstance(v, dict):
|
||||
v = v.get(CONF_XIAOMI_CLOUD)
|
||||
if isinstance(v, MiotCloud):
|
||||
cls[v.unique_id] = v
|
||||
return list(cls.values())
|
||||
|
||||
@staticmethod
|
||||
def get_random_string(length):
|
||||
seq = string.ascii_uppercase + string.digits
|
||||
return ''.join((random.choice(seq) for _ in range(length)))
|
||||
|
||||
|
||||
class MiCloudNeedVerify(MiCloudException):
|
||||
url = None
|
||||
|
||||
def with_url(self, url):
|
||||
self.url = url
|
||||
return self
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Support for Curtain and Airer."""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
CoverEntity as BaseEntity,
|
||||
CoverEntityFeature,
|
||||
CoverDeviceClass,
|
||||
)
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
from .core.miot_spec import MiotProperty
|
||||
from .core.converters import MiotPropConv, MiotTargetPositionConv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
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
|
||||
bind_services_to_entries(hass, SERVICE_TO_METHOD)
|
||||
|
||||
|
||||
class CoverEntity(XEntity, BaseEntity):
|
||||
_attr_is_closed = None
|
||||
_attr_device_class = 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 = (0, 100)
|
||||
_is_airer = None
|
||||
_motor_reverse = None
|
||||
_position_reverse = None
|
||||
_open_texts = ['open', 'up']
|
||||
_close_texts = ['close', 'down']
|
||||
_closed_position = 0
|
||||
_deviated_position = 0
|
||||
_target_position_props = None
|
||||
_cover_position_mapping = None
|
||||
_target2current_position = None
|
||||
|
||||
def on_init(self):
|
||||
self._attr_available = self.device.available
|
||||
|
||||
models = f'{self.device.model} {self.device.info.urn}'
|
||||
self._is_airer = 'airer' in models
|
||||
if self._is_airer:
|
||||
self._position_reverse = True
|
||||
elif 'curtain' in models:
|
||||
self._attr_device_class = CoverDeviceClass.CURTAIN
|
||||
elif 'wopener' in models or 'window-opener' in models:
|
||||
self._attr_device_class = CoverDeviceClass.WINDOW
|
||||
|
||||
self._motor_reverse = self.custom_config_bool('motor_reverse', self._motor_reverse)
|
||||
self._position_reverse = self.custom_config_bool('position_reverse', self._position_reverse)
|
||||
self._open_texts = self.custom_config_list('open_texts', self._open_texts)
|
||||
self._close_texts = self.custom_config_list('close_texts', self._close_texts)
|
||||
if self._motor_reverse:
|
||||
self._open_texts, self._close_texts = self._close_texts, self._open_texts
|
||||
self._target_position_props = self.custom_config_list('target_position_props') or []
|
||||
self._cover_position_mapping = self.custom_config_json('cover_position_mapping') or {}
|
||||
|
||||
if not self.conv.attrs:
|
||||
self.conv.attrs.append(self.conv.full_name)
|
||||
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(['status']):
|
||||
self._conv_status = conv
|
||||
elif prop.in_list(['motor_control']):
|
||||
self._conv_motor = conv
|
||||
self._attr_supported_features |= CoverEntityFeature.OPEN
|
||||
self._attr_supported_features |= CoverEntityFeature.CLOSE
|
||||
if prop.list_first('Stop', 'Pause') is not None:
|
||||
self._attr_supported_features |= CoverEntityFeature.STOP
|
||||
elif prop.in_list(['current_position']):
|
||||
if prop.value_range:
|
||||
self._conv_current_position = conv
|
||||
self._current_range = (prop.range_min(), prop.range_max())
|
||||
self.log.debug('current_position: %s', conv)
|
||||
elif prop.value_list and self._cover_position_mapping and not self._conv_current_position:
|
||||
self._conv_current_position = conv
|
||||
self._current_range = (0, 100)
|
||||
self.log.debug('current_position: %s', conv)
|
||||
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(self._target_position_props):
|
||||
self._conv_target_position = conv
|
||||
self._target_range = (prop.range_min(), prop.range_max())
|
||||
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
|
||||
|
||||
if self.custom_config_bool('disable_target_position'):
|
||||
self._conv_target_position = None
|
||||
self._attr_supported_features &= ~CoverEntityFeature.SET_POSITION
|
||||
|
||||
self._deviated_position = self.custom_config_integer('deviated_position', 2)
|
||||
if self._current_range:
|
||||
pos = self._current_range[0] + self._deviated_position
|
||||
self._closed_position = self.custom_config_integer('closed_position', pos)
|
||||
self._target2current_position = self.custom_config_bool('target2current_position', not self._conv_current_position)
|
||||
|
||||
if self._motor_reverse or self._position_reverse:
|
||||
self._attr_extra_state_attributes.update({
|
||||
'motor_reverse': self._motor_reverse,
|
||||
'position_reverse': self._position_reverse,
|
||||
})
|
||||
if self._closed_position:
|
||||
self._attr_extra_state_attributes.update({
|
||||
'closed_position': self._closed_position,
|
||||
'deviated_position': self._deviated_position,
|
||||
})
|
||||
|
||||
def set_state(self, data: dict):
|
||||
prop_status = getattr(self._conv_status, 'prop', None) if self._conv_status else None
|
||||
if prop_status:
|
||||
val = self._conv_status.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_is_closed = None
|
||||
self._attr_is_opening = None
|
||||
self._attr_is_closing = None
|
||||
if val in prop_status.list_search('Closed'):
|
||||
self._attr_is_closed = True
|
||||
elif val in prop_status.list_search('Opened'):
|
||||
self._attr_is_closed = False
|
||||
elif val in prop_status.list_search('Opening'):
|
||||
self._attr_is_opening = True
|
||||
elif val in prop_status.list_search('Closing'):
|
||||
self._attr_is_closing = True
|
||||
elif val in prop_status.list_search('Rising'):
|
||||
self._attr_is_closing = self._position_reverse
|
||||
elif val in prop_status.list_search('Falling', 'Dropping'):
|
||||
self._attr_is_opening = self._position_reverse
|
||||
elif val in prop_status.list_search('Stop Lower Limit', 'Stop At Lowest', 'Floor'):
|
||||
self._attr_is_closed = not self._position_reverse
|
||||
elif val in prop_status.list_search('Stop Upper Limit', 'Stop At Highest', 'Ceiling'):
|
||||
self._attr_is_closed = self._position_reverse
|
||||
elif self._is_airer and val in prop_status.list_search('Down'):
|
||||
self._attr_is_closed = False
|
||||
if self._conv_current_position:
|
||||
val = self._conv_current_position.value_from_dict(data)
|
||||
if val is not None:
|
||||
val = int(val)
|
||||
if self._cover_position_mapping:
|
||||
val = self._cover_position_mapping.get(val, val)
|
||||
if self._position_reverse:
|
||||
val = self._current_range[1] - val
|
||||
self._attr_current_cover_position = val
|
||||
if self._conv_target_position:
|
||||
val = self._conv_target_position.value_from_dict(data)
|
||||
if val is not None:
|
||||
val = int(val)
|
||||
if self._position_reverse:
|
||||
val = self._target_range[1] - val
|
||||
self._attr_target_cover_position = val
|
||||
if self._target2current_position:
|
||||
self._attr_current_cover_position = self._attr_target_cover_position
|
||||
self._attr_extra_state_attributes.update({
|
||||
'target2current_position': True,
|
||||
})
|
||||
if (val := self._attr_current_cover_position) is not None:
|
||||
closed = val <= self._closed_position
|
||||
if self._deviated_position is None:
|
||||
pass
|
||||
elif val <= self._deviated_position:
|
||||
self._attr_current_cover_position = 0
|
||||
closed = not self._is_airer
|
||||
elif val >= (100 - self._deviated_position):
|
||||
self._attr_current_cover_position = 100
|
||||
closed = self._is_airer
|
||||
if self._attr_is_closed is None or not prop_status:
|
||||
self._attr_is_closed = closed
|
||||
self._attr_extra_state_attributes.update({
|
||||
'state_is_closed': self._attr_is_closed,
|
||||
})
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
if conv := self._conv_motor:
|
||||
val = conv.prop.list_first(*self._open_texts)
|
||||
if val is not None:
|
||||
await self.device.async_write({conv.full_name: val})
|
||||
return
|
||||
self.log.warning('No open command found in motor control property: %s', self._open_texts)
|
||||
await self.async_set_cover_position(0 if self._position_reverse else 100)
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
if conv := self._conv_motor:
|
||||
val = conv.prop.list_first(*self._close_texts)
|
||||
if val is not None:
|
||||
await self.device.async_write({conv.full_name: val})
|
||||
return
|
||||
self.log.warning('No close command found in motor control property: %s', [self._close_texts, conv.prop.value_list])
|
||||
await self.async_set_cover_position(100 if self._position_reverse else 0)
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
if not self._conv_motor:
|
||||
return
|
||||
val = self._conv_motor.prop.list_first('Stop', 'Pause')
|
||||
if val is not None:
|
||||
await self.device.async_write({self._conv_motor.full_name: val})
|
||||
|
||||
async def async_set_cover_position(self, position, **kwargs):
|
||||
if not self._conv_target_position:
|
||||
return
|
||||
if self._position_reverse:
|
||||
position = self._target_range[1] - position
|
||||
await self.device.async_write({self._conv_target_position.full_name: position})
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = CoverEntity
|
||||
@@ -0,0 +1,248 @@
|
||||
"""Support for Xiaomi device tracker."""
|
||||
import logging
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.device_tracker.const import SourceType
|
||||
from homeassistant.components.device_tracker.config_entry import (
|
||||
TrackerEntity as BaseTrackerEntity,
|
||||
ScannerEntity as BaseScannerEntity,
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
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,
|
||||
)
|
||||
from .core.coord_transform import gcj02_to_wgs84, bd09_to_wgs84
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
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
|
||||
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('watch', 'rearview_mirror', 'head_up_display'):
|
||||
if 'xiaoxun.watch.' in model:
|
||||
entities.append(XiaoxunWatchTrackerEntity(config, srv))
|
||||
elif srv.get_property('latitude', 'longitude'):
|
||||
entities.append(MiotTrackerEntity(config, srv))
|
||||
if not entities and ('xiaoxun.watch.' in model or 'xiaoxun.tracker.' in model):
|
||||
# xiaoxun.watch.sw763
|
||||
# xiaoxun.tracker.v1
|
||||
entities.append(XiaoxunWatchTrackerEntity(config))
|
||||
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 ScannerEntity(XEntity, BaseScannerEntity, RestoreEntity):
|
||||
@property
|
||||
def device_info(self):
|
||||
return self._attr_device_info
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
return self._attr_unique_id
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
"""Return true if the device is connected to the network."""
|
||||
return True if self._attr_state else False
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
return SourceType.ROUTER
|
||||
|
||||
def get_state(self) -> dict:
|
||||
return {self.attr: self._attr_state}
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = ScannerEntity
|
||||
XEntity.CLS['scanner'] = ScannerEntity
|
||||
|
||||
|
||||
class MiotTrackerEntity(MiotEntity, BaseTrackerEntity):
|
||||
_attr_latitude = None
|
||||
_attr_longitude = None
|
||||
_attr_location_name = None
|
||||
_attr_location_accuracy = 0
|
||||
_disable_location_name = False
|
||||
|
||||
def __init__(self, config, miot_service: MiotService = None):
|
||||
super().__init__(miot_service, config=config, logger=_LOGGER)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self._disable_location_name = self.custom_config_bool('disable_location_name')
|
||||
|
||||
async def async_update(self):
|
||||
await super().async_update()
|
||||
if not self._available or not self._miot_service:
|
||||
return
|
||||
|
||||
if prop := self._miot_service.get_property('latitude'):
|
||||
self._attr_latitude = prop.from_device(self.device)
|
||||
if prop := self._miot_service.get_property('longitude'):
|
||||
self._attr_longitude = prop.from_device(self.device)
|
||||
if prop := self._miot_service.get_property('current_address'):
|
||||
self._attr_location_name = prop.from_device(self.device)
|
||||
await self.transform_coord()
|
||||
|
||||
async def transform_coord(self, default=None):
|
||||
if not (self._attr_latitude or self._attr_longitude):
|
||||
return
|
||||
typ = self.custom_config('coord_type') or default
|
||||
if not typ:
|
||||
return
|
||||
typ = f'{typ}'.lower()
|
||||
if typ == 'gcj02':
|
||||
self._attr_longitude, self._attr_latitude = gcj02_to_wgs84(self._attr_longitude, self._attr_latitude)
|
||||
if typ == 'bd09':
|
||||
self._attr_longitude, self._attr_latitude = bd09_to_wgs84(self._attr_longitude, self._attr_latitude)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling for entities that have location pushed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
return SourceType.GPS
|
||||
|
||||
@property
|
||||
def latitude(self):
|
||||
"""Return latitude value of the device."""
|
||||
return self._attr_latitude
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Return longitude value of the device."""
|
||||
return self._attr_longitude
|
||||
|
||||
@property
|
||||
def location_name(self):
|
||||
"""Return a location name for the current location of the device."""
|
||||
if self._disable_location_name:
|
||||
return None
|
||||
return self._attr_location_name
|
||||
|
||||
@property
|
||||
def location_accuracy(self):
|
||||
"""Return the location accuracy of the device.
|
||||
Value in meters.
|
||||
"""
|
||||
return self._attr_location_accuracy
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return the battery level of the device."""
|
||||
if not self._miot_service:
|
||||
return None
|
||||
sls = [self._miot_service, *self._miot_service.spec.get_services('battery')]
|
||||
for srv in sls:
|
||||
prop = srv.get_property('battery_level')
|
||||
if prop:
|
||||
return prop.from_device(self.device)
|
||||
return None
|
||||
|
||||
|
||||
class XiaoxunWatchTrackerEntity(MiotTrackerEntity):
|
||||
def __init__(self, config, miot_service: MiotService = None, miot_spec: MiotSpec = None):
|
||||
self._miot_spec = miot_spec
|
||||
super().__init__(config=config, miot_service=miot_service)
|
||||
|
||||
@property
|
||||
def device_eid(self):
|
||||
did = f'{self.miot_did}'
|
||||
return did.replace('xiaoxun.', '')
|
||||
|
||||
async def async_update(self):
|
||||
await super().async_update()
|
||||
await self.update_location()
|
||||
|
||||
async def update_location(self):
|
||||
did = f'{self.miot_did}'
|
||||
mic = self.xiaomi_cloud
|
||||
if not did or not mic:
|
||||
return
|
||||
pms = {
|
||||
'app_id': '10025',
|
||||
'dids': [did],
|
||||
'params': {
|
||||
'CID': 50031,
|
||||
'model': self.model,
|
||||
'SN': int(time.time() / 1000),
|
||||
'PL': {
|
||||
'Size': 1,
|
||||
'Key': '78999898989898998',
|
||||
'EID': self.device_eid,
|
||||
},
|
||||
},
|
||||
}
|
||||
rdt = await mic.async_request_api('third/api', pms) or {}
|
||||
loc = {}
|
||||
for v in (rdt.get('result') or {}).get('PL', {}).get('List', {}).values():
|
||||
if loc := v.get('result') or {}:
|
||||
loc.setdefault('device', v)
|
||||
loc.setdefault('timestamp', v.get('timestamp', ''))
|
||||
break
|
||||
if not loc:
|
||||
self.logger.warning('%s: Got xiaoxun watch location faild: %s', self.name_model, rdt)
|
||||
return
|
||||
self.logger.debug('%s: Got xiaoxun watch location: %s', self.name_model, rdt)
|
||||
dvc = loc.get('device') or {}
|
||||
gps = f"{loc.get('location', '')},".split(',')
|
||||
self._attr_latitude = float(gps[1])
|
||||
self._attr_longitude = float(gps[0])
|
||||
self._attr_location_name = loc.get('desc')
|
||||
self._attr_location_accuracy = int(loc.get('radius') or 0)
|
||||
await self.transform_coord(default='gcj02')
|
||||
self.update_attrs({
|
||||
'sos': dvc.get('SOS', 0),
|
||||
'steps': dvc.get('steps', 0),
|
||||
'home_wifi': dvc.get('home_wifi', 0),
|
||||
'imei': dvc.get('imei'),
|
||||
'adcode': loc.get('adcode'),
|
||||
'country': loc.get('country'),
|
||||
'province': loc.get('province'),
|
||||
'city': loc.get('province'),
|
||||
'district': loc.get('district'),
|
||||
'township': loc.get('township'),
|
||||
'road': loc.get('road'),
|
||||
'street': loc.get('street'),
|
||||
})
|
||||
if tim := loc.get('timestamp', ''):
|
||||
self.update_attrs({
|
||||
'timestamp': f'{tim[0:4]}-{tim[4:6]}-{tim[6:8]} {tim[8:10]}:{tim[10:12]}:{tim[12:14]}',
|
||||
})
|
||||
@@ -0,0 +1,278 @@
|
||||
"""Support for Xiaomi fans."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
FanEntity as BaseEntity,
|
||||
FanEntityFeature,
|
||||
)
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
MiirToggleEntity,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
from .core.miot_spec import (
|
||||
MiotSpec,
|
||||
MiotService,
|
||||
MiotProperty,
|
||||
)
|
||||
from homeassistant.util.percentage import (
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item,
|
||||
ranged_value_to_percentage,
|
||||
percentage_to_ranged_value,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
SERVICE_TO_METHOD = {}
|
||||
|
||||
SPEED_OFF = 'off'
|
||||
OFF_SPEED_VALUES = [SPEED_OFF, None]
|
||||
|
||||
|
||||
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
|
||||
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('ir_fan_control'):
|
||||
entities.append(MiirFanEntity(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 FanEntity(XEntity, BaseEntity):
|
||||
_attr_device_class = None
|
||||
_conv_power = None
|
||||
_conv_mode = None
|
||||
_conv_speed = None
|
||||
_speed_list = None
|
||||
_speed_range = None
|
||||
_conv_oscillate = None
|
||||
_prop_speed = None
|
||||
_prop_percentage = None
|
||||
|
||||
def on_init(self):
|
||||
speed_property = self.custom_config('speed_property', 'speed')
|
||||
percentage_property = self.custom_config('percentage_property', 'fan_percent')
|
||||
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
|
||||
if hasattr(FanEntityFeature, 'TURN_ON'): # v2024.8
|
||||
self._attr_supported_features |= FanEntityFeature.TURN_ON
|
||||
if hasattr(FanEntityFeature, 'TURN_OFF'):
|
||||
self._attr_supported_features |= FanEntityFeature.TURN_OFF
|
||||
elif prop.in_list(['mode']):
|
||||
self._conv_mode = conv
|
||||
self._attr_preset_modes = prop.list_descriptions()
|
||||
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
|
||||
elif prop.in_list(['fan_level', 'speed_level', 'stepless_fan_level', speed_property, percentage_property]):
|
||||
if prop.value_range:
|
||||
self.set_percentage_property(prop)
|
||||
elif prop.value_list and not self._conv_speed:
|
||||
self.set_speeds_property(prop)
|
||||
else:
|
||||
continue
|
||||
self._conv_speed = conv
|
||||
elif prop.in_list(['horizontal_swing', 'vertical_swing']) and not self._conv_oscillate:
|
||||
self._conv_oscillate = conv
|
||||
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
||||
|
||||
# issues/617
|
||||
if self.custom_config_bool('disable_preset_modes'):
|
||||
self._attr_supported_features &= ~FanEntityFeature.PRESET_MODE
|
||||
self._attr_preset_modes = []
|
||||
elif dpm := self.custom_config_list('disable_preset_modes'):
|
||||
self._attr_preset_modes = [
|
||||
mode
|
||||
for mode in self._attr_preset_modes
|
||||
if mode not in dpm
|
||||
]
|
||||
|
||||
def set_percentage_property(self, prop):
|
||||
if not prop or not prop.value_range:
|
||||
return
|
||||
self._prop_percentage = prop
|
||||
_min = prop.range_min()
|
||||
_max = prop.range_max()
|
||||
_stp = prop.range_step()
|
||||
self._speed_range = (_min, _max)
|
||||
self._attr_speed_count = (_max - _min) / _stp + 1
|
||||
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||
|
||||
def set_speeds_property(self, prop):
|
||||
if not prop:
|
||||
return
|
||||
self._speed_list = prop.list_descriptions()
|
||||
self._attr_speed_count = len(self._speed_list)
|
||||
self._attr_extra_state_attributes['speed_list'] = self._speed_list
|
||||
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||
|
||||
def set_state(self, data: dict):
|
||||
if self._conv_speed:
|
||||
val = self._conv_speed.value_from_dict(data)
|
||||
if val is not None:
|
||||
des = self._conv_speed.prop.list_description(val)
|
||||
if self._speed_range:
|
||||
self._attr_percentage = ranged_value_to_percentage(self._speed_range, float(val))
|
||||
if self._speed_list and val in self._speed_list:
|
||||
self._attr_percentage = ordered_list_item_to_percentage(self._speed_list, val)
|
||||
elif self._speed_list and des in self._speed_list:
|
||||
self._attr_percentage = ordered_list_item_to_percentage(self._speed_list, des)
|
||||
if self._conv_power:
|
||||
val = self._conv_power.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_is_on = bool(val)
|
||||
if not self._attr_is_on:
|
||||
self._attr_percentage = 0
|
||||
if self._conv_mode:
|
||||
val = self._conv_mode.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_preset_mode = val
|
||||
if self._conv_oscillate:
|
||||
val = self._conv_oscillate.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_oscillating = bool(val)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
if self._conv_power:
|
||||
return self._attr_is_on
|
||||
return self.percentage is not None and self.percentage > 0
|
||||
|
||||
async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs):
|
||||
dat = {}
|
||||
if self._conv_power and not self.is_on:
|
||||
dat[self._conv_power.full_name] = True
|
||||
if percentage is not None:
|
||||
if self._speed_range:
|
||||
dat[self._conv_speed.full_name] = percentage_to_ranged_value(self._speed_range, percentage)
|
||||
elif self._speed_list:
|
||||
des = percentage_to_ordered_list_item(self._speed_list, percentage)
|
||||
dat[self._conv_speed.full_name] = des
|
||||
if preset_mode is not None:
|
||||
dat[self._conv_mode.full_name] = preset_mode
|
||||
if dat:
|
||||
await self.device.async_write(dat)
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
if not self._conv_power:
|
||||
return
|
||||
await self.device.async_write({self._conv_power.full_name: False})
|
||||
|
||||
async def async_set_percentage(self, percentage: int):
|
||||
if percentage == 0 and self._conv_power:
|
||||
await self.async_turn_off()
|
||||
return
|
||||
await self.async_turn_on(percentage=percentage)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str):
|
||||
if not self._conv_mode:
|
||||
return
|
||||
await self.device.async_write({self._conv_mode.full_name: preset_mode})
|
||||
|
||||
async def async_oscillate(self, oscillating: bool):
|
||||
if not self._conv_oscillate:
|
||||
return
|
||||
await self.device.async_write({self._conv_oscillate.full_name: oscillating})
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = FanEntity
|
||||
|
||||
|
||||
class MiirFanEntity(MiirToggleEntity, FanEntity):
|
||||
def __init__(self, config: dict, miot_service: MiotService):
|
||||
super().__init__(miot_service, config=config, logger=_LOGGER)
|
||||
|
||||
if self._act_turn_on and hasattr(FanEntityFeature, 'TURN_ON'): # v2024.8
|
||||
self._supported_features |= FanEntityFeature.TURN_ON
|
||||
if self._act_turn_off and hasattr(FanEntityFeature, 'TURN_OFF'):
|
||||
self._supported_features |= FanEntityFeature.TURN_OFF
|
||||
|
||||
self._attr_percentage = 50
|
||||
self._act_speed_up = miot_service.get_action('fan_speed_up')
|
||||
self._act_speed_dn = miot_service.get_action('fan_speed_down')
|
||||
if self._act_speed_up or self._act_speed_dn:
|
||||
self._supported_features |= FanEntityFeature.SET_SPEED
|
||||
|
||||
self._act_swing_on = miot_service.get_action('horizontal_swing_on')
|
||||
self._act_swing_off = miot_service.get_action('horizontal_swing_off')
|
||||
if self._act_swing_on or self._act_swing_off:
|
||||
self._supported_features |= FanEntityFeature.OSCILLATE
|
||||
|
||||
self._supported_features |= FanEntityFeature.PRESET_MODE
|
||||
self._attr_preset_mode = None
|
||||
self._attr_preset_modes = []
|
||||
for a in miot_service.actions.values():
|
||||
if a.ins:
|
||||
continue
|
||||
self._attr_preset_modes.append(a.friendly_desc)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
# issues/617
|
||||
if self.custom_config_bool('disable_preset_modes'):
|
||||
self._supported_features &= ~FanEntityFeature.PRESET_MODE
|
||||
self._attr_preset_modes = []
|
||||
elif dpm := self.custom_config_list('disable_preset_modes'):
|
||||
self._attr_preset_modes = [
|
||||
mode
|
||||
for mode in self._attr_preset_modes
|
||||
if mode not in dpm
|
||||
]
|
||||
|
||||
async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
if percentage is None:
|
||||
pass
|
||||
elif percentage > self._attr_percentage and self._act_speed_up:
|
||||
return await self.async_call_action(self._act_speed_up)
|
||||
elif percentage < self._attr_percentage and self._act_speed_dn:
|
||||
return await self.async_call_action(self._act_speed_dn)
|
||||
|
||||
if preset_mode is None:
|
||||
pass
|
||||
elif act := self._miot_service.get_action(preset_mode):
|
||||
return await self.async_call_action(act)
|
||||
|
||||
return await super().async_turn_on(**kwargs)
|
||||
|
||||
async def async_set_percentage(self, percentage: int):
|
||||
"""Set the speed of the fan, as a percentage."""
|
||||
return await self.async_turn_on(percentage=percentage)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
"""Set new preset mode."""
|
||||
return await self.async_turn_on(preset_mode=preset_mode)
|
||||
|
||||
async def async_oscillate(self, oscillating):
|
||||
"""Oscillate the fan."""
|
||||
ret = None
|
||||
if not oscillating and self._act_swing_off:
|
||||
ret = await self.async_call_action(self._act_swing_off)
|
||||
elif oscillating and self._act_swing_on:
|
||||
ret = await self.async_call_action(self._act_swing_on)
|
||||
if ret:
|
||||
self._attr_oscillating = oscillating
|
||||
return ret
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Support for humidifier and dehumidifier."""
|
||||
import logging
|
||||
from bisect import bisect
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
HumidifierEntity as BaseEntity,
|
||||
HumidifierEntityFeature, # v2022.5
|
||||
HumidifierDeviceClass,
|
||||
)
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
from .core.miot_spec import MiotProperty
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
MODE_OFF = 'Off'
|
||||
|
||||
|
||||
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):
|
||||
bind_services_to_entries(hass, {})
|
||||
|
||||
|
||||
class HumidifierEntity(XEntity, BaseEntity):
|
||||
_attr_mode = None
|
||||
_attr_available_modes = None
|
||||
_conv_power = None
|
||||
_conv_mode = None
|
||||
_conv_target_humidity = None
|
||||
_conv_current_humidity = None
|
||||
_target_humidity_ratio = 1
|
||||
_target_humidity_step = None
|
||||
|
||||
def on_init(self):
|
||||
self._attr_available_modes = []
|
||||
self._target_humidity_ratio = self.custom_config_number('target_humidity_ratio', 1)
|
||||
|
||||
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_available_modes.append(MODE_OFF)
|
||||
self._attr_supported_features |= HumidifierEntityFeature.MODES
|
||||
elif prop.in_list(['mode', 'fan_level']) and not self._conv_mode:
|
||||
self._conv_mode = conv
|
||||
self._attr_available_modes.extend(prop.list_descriptions())
|
||||
self._attr_supported_features |= HumidifierEntityFeature.MODES
|
||||
elif prop.in_list(['relative_humidity', 'humidity']):
|
||||
self._conv_current_humidity = conv
|
||||
elif prop.in_list(['target_humidity']):
|
||||
self._conv_target_humidity = conv
|
||||
if prop.value_range:
|
||||
self._attr_min_humidity = prop.range_min()
|
||||
self._attr_max_humidity = prop.range_max()
|
||||
self._target_humidity_step = prop.range_step()
|
||||
elif prop.value_list:
|
||||
vls = prop.list_value(None)
|
||||
vls.sort()
|
||||
self._attr_min_humidity = vls[0]
|
||||
self._attr_max_humidity = vls[-1]
|
||||
if self._target_humidity_ratio:
|
||||
self._attr_min_humidity = round(self._attr_min_humidity * self._target_humidity_ratio)
|
||||
self._attr_max_humidity = round(self._attr_max_humidity * self._target_humidity_ratio)
|
||||
|
||||
typ = f'{self.model} {self._miot_service.spec.type}'
|
||||
if 'dehumidifier' in typ or '.derh.' in typ:
|
||||
self._attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER
|
||||
else:
|
||||
self._attr_device_class = HumidifierDeviceClass.HUMIDIFIER
|
||||
|
||||
def set_state(self, data: dict):
|
||||
if self._conv_mode:
|
||||
val = self._conv_mode.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_mode = val
|
||||
if self._conv_power:
|
||||
val = self._conv_power.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_is_on = bool(val)
|
||||
if not self._attr_is_on:
|
||||
self._attr_mode = MODE_OFF
|
||||
if self._conv_target_humidity:
|
||||
val = self._conv_target_humidity.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_target_humidity = round(val * self._target_humidity_ratio)
|
||||
if self._conv_current_humidity:
|
||||
val = self._conv_current_humidity.value_from_dict(data)
|
||||
if val is not None:
|
||||
self._attr_current_humidity = val
|
||||
|
||||
async def async_turn_on(self):
|
||||
if not self._conv_power:
|
||||
return
|
||||
await self.device.async_write({self._conv_power.full_name: True})
|
||||
|
||||
async def async_turn_off(self):
|
||||
if not self._conv_power:
|
||||
return
|
||||
await self.device.async_write({self._conv_power.full_name: False})
|
||||
|
||||
async def async_set_mode(self, mode: str):
|
||||
if mode == MODE_OFF:
|
||||
await self.async_turn_off()
|
||||
return
|
||||
data = {}
|
||||
if not self._attr_is_on and self._conv_power:
|
||||
data[self._conv_power.full_name] = True
|
||||
if self._conv_mode:
|
||||
data[self._conv_mode.full_name] = mode
|
||||
if data:
|
||||
await self.device.async_write(data)
|
||||
|
||||
async def async_set_humidity(self, humidity: int):
|
||||
if not self._conv_target_humidity:
|
||||
return
|
||||
data = {}
|
||||
if not self._attr_is_on and self._conv_power:
|
||||
data[self._conv_power.full_name] = True
|
||||
|
||||
prop = getattr(self._conv_target_humidity, 'prop', None)
|
||||
if self._target_humidity_step:
|
||||
humidity = round(humidity / self._target_humidity_step) * self._target_humidity_step
|
||||
elif prop and prop.value_list:
|
||||
vls = prop.list_value(None)
|
||||
vls.sort()
|
||||
idx = bisect(vls, humidity)
|
||||
humidity = vls[idx - 1] if idx > 0 else vls[0]
|
||||
if self._target_humidity_ratio:
|
||||
humidity = round(humidity / self._target_humidity_ratio)
|
||||
|
||||
data[self._conv_target_humidity.full_name] = humidity
|
||||
await self.device.async_write(data)
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = HumidifierEntity
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"heater": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"warm": "mdi:weather-sunny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"fan": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"auto": "mdi:fan-auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"service": "mdi:powershell"
|
||||
},
|
||||
"get_properties": {
|
||||
"service": "mdi:magnify"
|
||||
},
|
||||
"set_property": {
|
||||
"service": "mdi:gamepad"
|
||||
},
|
||||
"set_miot_property": {
|
||||
"service": "mdi:gamepad-up"
|
||||
},
|
||||
"call_action": {
|
||||
"service": "mdi:gesture-tap"
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"service": "mdi:speaker-message"
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"service": "mdi:chat-sleep"
|
||||
},
|
||||
"renew_devices": {
|
||||
"service": "mdi:autorenew"
|
||||
},
|
||||
"get_token": {
|
||||
"service": "mdi:key-chain"
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"service": "mdi:cloud"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import Platform, ATTR_ENTITY_ID
|
||||
from homeassistant.helpers import intent
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_intents(hass: HomeAssistant):
|
||||
"""Set up the intents."""
|
||||
intent.async_register(hass, XiaoaiPlayText())
|
||||
intent.async_register(hass, XiaoaiExecuteCommand())
|
||||
|
||||
|
||||
def match_states_from_slots(intent_obj, slots, domains=None):
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=slots.get('name', {}).get('value'),
|
||||
area_name=slots.get('area', {}).get('value'),
|
||||
floor_name=slots.get('floor', {}).get('value'),
|
||||
domains=domains,
|
||||
assistant=intent_obj.assistant,
|
||||
single_target=True,
|
||||
)
|
||||
match_preferences = intent.MatchTargetsPreferences(
|
||||
area_id=slots.get('preferred_area_id', {}).get('value'),
|
||||
floor_id=slots.get('preferred_floor_id', {}).get('value'),
|
||||
)
|
||||
match_result = intent.async_match_targets(
|
||||
intent_obj.hass, match_constraints, match_preferences
|
||||
)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
)
|
||||
assert match_result.states
|
||||
return match_result.states
|
||||
|
||||
|
||||
class XiaoaiPlayText(intent.IntentHandler):
|
||||
intent_type = 'XiaoaiPlayText'
|
||||
description = 'Convert text to sound and play on the Xiaoai speaker or Xiaomi TV'
|
||||
slot_schema = {
|
||||
vol.Required('message', description='Message content'): intent.non_empty_string,
|
||||
vol.Optional('name', description='Name of the Xiaoai speaker or Xiaomi TV'): intent.non_empty_string,
|
||||
vol.Optional('area'): intent.non_empty_string,
|
||||
vol.Optional('floor'): intent.non_empty_string,
|
||||
vol.Optional('preferred_area_id'): str,
|
||||
vol.Optional('preferred_floor_id'): str,
|
||||
}
|
||||
platforms = {Platform.MEDIA_PLAYER}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent):
|
||||
"""Handle the intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
states = match_states_from_slots(intent_obj, slots, [Platform.MEDIA_PLAYER])
|
||||
state = states[0]
|
||||
for sta in states:
|
||||
if 'xiaomi' in state.entity_id:
|
||||
state = sta
|
||||
break
|
||||
|
||||
message = slots.get('message', {}).get('value')
|
||||
await hass.services.async_call(
|
||||
'xiaomi_miot',
|
||||
'intelligent_speaker',
|
||||
target={ATTR_ENTITY_ID: state.entity_id},
|
||||
service_data={
|
||||
'text': message,
|
||||
'execute': False,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||
response.async_set_results(
|
||||
success_results=[
|
||||
intent.IntentResponseTarget(
|
||||
name=str(state.name),
|
||||
type=intent.IntentResponseTargetType.ENTITY,
|
||||
id=state.entity_id,
|
||||
),
|
||||
],
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class XiaoaiExecuteCommand(intent.IntentHandler):
|
||||
intent_type = 'XiaoaiExecuteCommand'
|
||||
description = 'Execute commands on the Xiaoai speaker or Xiaomi TV'
|
||||
slot_schema = {
|
||||
vol.Required('command', description='Command content'): intent.non_empty_string,
|
||||
vol.Optional('name', description='Name of the Xiaoai speaker or Xiaomi TV'): intent.non_empty_string,
|
||||
vol.Optional('area'): intent.non_empty_string,
|
||||
vol.Optional('floor'): intent.non_empty_string,
|
||||
vol.Optional('preferred_area_id'): str,
|
||||
vol.Optional('preferred_floor_id'): str,
|
||||
}
|
||||
platforms = {Platform.MEDIA_PLAYER}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent):
|
||||
"""Handle the intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
states = match_states_from_slots(intent_obj, slots, [Platform.MEDIA_PLAYER])
|
||||
state = states[0]
|
||||
for sta in states:
|
||||
if 'xiaomi' in state.entity_id:
|
||||
state = sta
|
||||
break
|
||||
|
||||
command = slots.get('command', {}).get('value')
|
||||
await hass.services.async_call(
|
||||
'xiaomi_miot',
|
||||
'intelligent_speaker',
|
||||
target={ATTR_ENTITY_ID: state.entity_id},
|
||||
service_data={
|
||||
'text': command,
|
||||
'execute': True,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||
response.async_set_results(
|
||||
success_results=[
|
||||
intent.IntentResponseTarget(
|
||||
name=str(state.name),
|
||||
type=intent.IntentResponseTargetType.ENTITY,
|
||||
id=state.entity_id,
|
||||
),
|
||||
],
|
||||
)
|
||||
return response
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Support for Xiaomi lights."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
LightEntity as BaseEntity,
|
||||
LightEntityFeature, # v2022.5
|
||||
ColorMode,
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_EFFECT,
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
MiirToggleEntity,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
from .core.converters import (
|
||||
MiotBrightnessConv,
|
||||
MiotColorTempConv,
|
||||
MiotRgbColorConv,
|
||||
)
|
||||
from .core.miot_spec import (
|
||||
MiotSpec,
|
||||
MiotService,
|
||||
MiotProperty,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
|
||||
SERVICE_TO_METHOD = {}
|
||||
ATTR_COLOR_TEMP = 'color_temp'
|
||||
|
||||
|
||||
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
|
||||
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('ir_light_control'):
|
||||
if srv.name in ['ir_light_control']:
|
||||
entities.append(MiirLightEntity(config, srv))
|
||||
continue
|
||||
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 LightEntity(XEntity, BaseEntity, RestoreEntity):
|
||||
_attr_names = None
|
||||
_brightness_for_on = None
|
||||
_brightness_for_off = None
|
||||
|
||||
def on_init(self):
|
||||
self._attr_names = {}
|
||||
self._brightness_for_on = self.custom_config_number('brightness_for_on')
|
||||
self._brightness_for_off = self.custom_config_number('brightness_for_off')
|
||||
mode_property = self.custom_config('mode_property', 'mode')
|
||||
color_property = self.custom_config('color_property', 'color')
|
||||
color_temp_property = self.custom_config('color_temp_property', 'color_temp')
|
||||
brightness_property = self.custom_config('brightness_property', 'brightness')
|
||||
self._attr_color_mode = ColorMode.ONOFF
|
||||
|
||||
modes = set()
|
||||
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
|
||||
if prop.in_list(['brightness', brightness_property]) or isinstance(conv, MiotBrightnessConv):
|
||||
self._attr_names[ATTR_BRIGHTNESS] = attr
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
elif prop.in_list(['color_temperature', color_temp_property]) or isinstance(conv, MiotColorTempConv):
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
modes.add(ColorMode.COLOR_TEMP)
|
||||
|
||||
# percentage may be incorrectly declared as kelvin
|
||||
# e.g. https://home.miot-spec.com/spec/mrbond.airer.m33a
|
||||
if prop.unit == 'percentage' or prop.range_max() == 100:
|
||||
self._attr_min_color_temp_kelvin = MiotColorTempConv.percentage_to_kelvin(prop.range_max())
|
||||
self._attr_max_color_temp_kelvin = MiotColorTempConv.percentage_to_kelvin(prop.range_min())
|
||||
self._attr_names[ATTR_COLOR_TEMP_KELVIN] = attr
|
||||
elif prop.unit in ['kelvin']:
|
||||
self._attr_min_color_temp_kelvin = prop.range_min()
|
||||
self._attr_max_color_temp_kelvin = prop.range_max()
|
||||
self._attr_names[ATTR_COLOR_TEMP_KELVIN] = attr
|
||||
else:
|
||||
self._attr_min_mireds = prop.range_min()
|
||||
self._attr_max_mireds = prop.range_max()
|
||||
self._attr_names[ATTR_COLOR_TEMP] = attr
|
||||
elif prop.in_list(['color', color_property]) or isinstance(conv, MiotRgbColorConv):
|
||||
self._attr_names[ATTR_RGB_COLOR] = attr
|
||||
modes.add(ColorMode.RGB)
|
||||
elif prop.in_list(['mode', 'color_mode', mode_property]):
|
||||
self._attr_names[ATTR_EFFECT] = attr
|
||||
self._attr_effect_list = prop.list_descriptions()
|
||||
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||
|
||||
self._attr_supported_color_modes = modes if modes else {self._attr_color_mode}
|
||||
|
||||
def get_state(self) -> dict:
|
||||
return {
|
||||
self.attr: self._attr_is_on,
|
||||
ATTR_BRIGHTNESS: self._attr_brightness,
|
||||
ATTR_COLOR_TEMP: self._attr_color_temp,
|
||||
}
|
||||
|
||||
def set_state(self, data: dict):
|
||||
val = data.get(self.attr)
|
||||
if val is not None:
|
||||
self._attr_is_on = bool(val)
|
||||
|
||||
if (val := data.get(self._attr_names.get(ATTR_BRIGHTNESS))) is not None:
|
||||
self._attr_brightness = val
|
||||
if self._brightness_for_on is not None:
|
||||
self._attr_is_on = val >= self._brightness_for_on
|
||||
if (val := data.get(self._attr_names.get(ATTR_COLOR_TEMP_KELVIN))) is not None:
|
||||
if val != self._attr_color_temp_kelvin:
|
||||
self._attr_color_temp_kelvin = val
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
elif (val := data.get(self._attr_names.get(ATTR_COLOR_TEMP))) is not None:
|
||||
if val != self._attr_color_temp:
|
||||
self._attr_color_temp = val
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
if (val := data.get(self._attr_names.get(ATTR_RGB_COLOR))) is not None:
|
||||
if val != self._attr_rgb_color:
|
||||
self._attr_rgb_color = val
|
||||
self._attr_color_mode = ColorMode.RGB
|
||||
if (val := data.get(self._attr_names.get(ATTR_EFFECT))) is not None:
|
||||
self._attr_effect = val
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
dat = {self.attr: True}
|
||||
if self._brightness_for_on is not None:
|
||||
dat[self.attr] = self._brightness_for_on
|
||||
for k, v in kwargs.items():
|
||||
if attr := self._attr_names.get(k):
|
||||
dat[attr] = v
|
||||
await self.device.async_write(dat)
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
dat = {self.attr: False}
|
||||
if self._brightness_for_off is not None:
|
||||
dat[self.attr] = self._brightness_for_off
|
||||
await self.device.async_write(dat)
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = LightEntity
|
||||
|
||||
|
||||
class MiirLightEntity(MiirToggleEntity, BaseEntity):
|
||||
def __init__(self, config: dict, miot_service: MiotService):
|
||||
super().__init__(miot_service, config=config, logger=_LOGGER)
|
||||
|
||||
self._act_bright_up = miot_service.get_action('brightness_up')
|
||||
self._act_bright_dn = miot_service.get_action('brightness_down')
|
||||
if self._act_bright_up or self._act_bright_dn:
|
||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
self._attr_brightness = 127
|
||||
|
||||
self._supported_features = LightEntityFeature.EFFECT
|
||||
self._attr_effect_list = []
|
||||
for a in miot_service.actions.values():
|
||||
if a.ins:
|
||||
continue
|
||||
self._attr_effect_list.append(a.friendly_desc)
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
bright = kwargs.get(ATTR_BRIGHTNESS)
|
||||
if bright is None:
|
||||
pass
|
||||
elif bright > self._attr_brightness and self._act_bright_up:
|
||||
return await self.async_call_action(self._act_bright_up)
|
||||
elif bright < self._attr_brightness and self._act_bright_dn:
|
||||
return await self.async_call_action(self._act_bright_dn)
|
||||
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
if act := self._miot_service.get_action(effect):
|
||||
return await self.async_call_action(act)
|
||||
|
||||
return await super().async_turn_on(**kwargs)
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"domain": "xiaomi_miot",
|
||||
"name": "Xiaomi Miot",
|
||||
"after_dependencies": ["homekit"],
|
||||
"codeowners": ["@al-one"],
|
||||
"config_flow": true,
|
||||
"dependencies": [
|
||||
"http",
|
||||
"intent",
|
||||
"persistent_notification",
|
||||
"ffmpeg"
|
||||
],
|
||||
"documentation": "https://github.com/al-one/hass-xiaomi-miot",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/al-one/hass-xiaomi-miot/issues",
|
||||
"requirements": [
|
||||
"construct>=2.10.68",
|
||||
"python-miio>=0.5.12",
|
||||
"micloud>=0.5"
|
||||
],
|
||||
"version": "1.1.2"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
"""Support number entity for Xiaomi Miot."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.number import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
RestoreNumber,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
|
||||
_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[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities
|
||||
bind_services_to_entries(hass, SERVICE_TO_METHOD)
|
||||
|
||||
|
||||
class NumberEntity(XEntity, RestoreNumber):
|
||||
_attr_mode = NumberMode.AUTO
|
||||
|
||||
def on_init(self):
|
||||
if self._miot_property:
|
||||
self._attr_native_step = self._miot_property.range_step()
|
||||
self._attr_native_max_value = self._miot_property.range_max()
|
||||
self._attr_native_min_value = self._miot_property.range_min()
|
||||
self._attr_native_unit_of_measurement = self._miot_property.unit_of_measurement
|
||||
|
||||
def get_state(self) -> dict:
|
||||
return {self.attr: self._attr_native_value}
|
||||
|
||||
def set_state(self, data: dict):
|
||||
val = self.conv.value_from_dict(data)
|
||||
if val is None:
|
||||
return
|
||||
self._attr_native_value = val
|
||||
|
||||
async def async_set_native_value(self, value: float):
|
||||
await self.device.async_write({self.attr: value})
|
||||
|
||||
if self._miot_action:
|
||||
self._attr_native_value = None
|
||||
async_call_later(self.hass, 0.5, self.schedule_update_ha_state)
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = NumberEntity
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Support remote entity for Xiaomi Miot."""
|
||||
import logging
|
||||
import asyncio
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_TOKEN,
|
||||
)
|
||||
from homeassistant.components import remote, persistent_notification
|
||||
from homeassistant.components.remote import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
RemoteEntity,
|
||||
RemoteEntityFeature,
|
||||
)
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
MiotEntity,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
from .core.utils import get_translations, DeviceException
|
||||
from .core.miot_spec import (
|
||||
MiotSpec,
|
||||
)
|
||||
from .core.xiaomi_cloud import (
|
||||
MiotCloud,
|
||||
MiCloudException,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
|
||||
|
||||
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
|
||||
model = str(config.get(CONF_MODEL) or '')
|
||||
spec = hass.data[DOMAIN]['miot_specs'].get(model)
|
||||
entities = []
|
||||
if isinstance(spec, MiotSpec):
|
||||
host = config.get(CONF_HOST)
|
||||
token = config.get(CONF_TOKEN)
|
||||
if spec.name in ['remote_control', 'ir_remote_control']:
|
||||
if 'chuangmi.remote.' in model or 'chuangmi.ir.' in model:
|
||||
ChuangmiIr = await hass.async_add_executor_job(import_miio_chuangmi_remote)
|
||||
device = ChuangmiIr(host, token)
|
||||
entities.append(MiotRemoteEntity(config, spec, device))
|
||||
elif model in [
|
||||
'xiaomi.wifispeaker.l05c',
|
||||
'xiaomi.wifispeaker.lx5a',
|
||||
'xiaomi.wifispeaker.lx06',
|
||||
'xiaomi.wifispeaker.oh2p',
|
||||
'lumi.acpartner.v1',
|
||||
'lumi.acpartner.v2',
|
||||
'lumi.acpartner.v3',
|
||||
'lumi.acpartner.mcn04',
|
||||
]:
|
||||
entities.append(MiotRemoteEntity(config, spec))
|
||||
for entity in entities:
|
||||
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
def import_miio_chuangmi_remote():
|
||||
try:
|
||||
from miio import ChuangmiIr
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
from miio.integrations.chuangmi.remote import ChuangmiIr
|
||||
return ChuangmiIr
|
||||
|
||||
|
||||
class MiotRemoteEntity(MiotEntity, RemoteEntity):
|
||||
def __init__(self, config, miot_spec: MiotSpec, device=None):
|
||||
self._miot_spec = miot_spec
|
||||
super().__init__(miot_service=None, device=device, config=config, logger=_LOGGER)
|
||||
self._attr_should_poll = False
|
||||
self._supported_features = RemoteEntityFeature.LEARN_COMMAND
|
||||
self._translations = get_translations('ir_devices')
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
did = self.miot_did
|
||||
mic = self.miot_cloud
|
||||
irs = []
|
||||
if did and isinstance(mic, MiotCloud):
|
||||
rdt = await mic.async_request_api('v2/irdevice/controllers', {'parent_id': did}) or {}
|
||||
rds = (rdt.get('result') or {}).get('controllers') or []
|
||||
if not rdt:
|
||||
dls = await mic.async_get_devices() or []
|
||||
for d in dls:
|
||||
if did == d.get('parent_id'):
|
||||
rds.append(d)
|
||||
for d in rds:
|
||||
ird = d.get('did')
|
||||
rdt = await mic.async_request_api('v2/irdevice/controller/keys', {'did': ird}) or {}
|
||||
kys = (rdt.get('result') or {}).get('keys', {})
|
||||
irs.append({
|
||||
'did': ird,
|
||||
'name': d.get('name'),
|
||||
'keys': kys,
|
||||
})
|
||||
add_selects = self._add_entities.get('select')
|
||||
if not kys:
|
||||
self.logger.info('%s: IR device %s(%s) have no keys: %s', self.name_model, ird, d.get('name'), rdt)
|
||||
elif add_selects and ird not in self._subs:
|
||||
from .select import SelectSubEntity
|
||||
ols = []
|
||||
for k in kys:
|
||||
nam = k.get('display_name') or k.get('name')
|
||||
if not nam:
|
||||
continue
|
||||
nam = self._translations.get(nam, nam)
|
||||
ols.append(nam)
|
||||
self._subs[ird] = SelectSubEntity(self, ird, option={
|
||||
'name': d.get('name'),
|
||||
'entity_id': f'remote_{ird}'.replace('.', '_'),
|
||||
'options': ols,
|
||||
'async_select_option': self.async_press_ir_key,
|
||||
})
|
||||
add_selects([self._subs[ird]], update_before_add=False)
|
||||
if irs:
|
||||
self._state_attrs['ir_devices'] = irs
|
||||
|
||||
def is_on(self):
|
||||
return True
|
||||
|
||||
async def async_send_command(self, command, **kwargs):
|
||||
"""Send commands to a device."""
|
||||
repeat = kwargs.get(remote.ATTR_NUM_REPEATS, remote.DEFAULT_NUM_REPEATS)
|
||||
delays = kwargs.get(remote.ATTR_DELAY_SECS, remote.DEFAULT_DELAY_SECS)
|
||||
did = kwargs.get(remote.ATTR_DEVICE)
|
||||
for _ in range(repeat):
|
||||
for cmd in command:
|
||||
try:
|
||||
if f'{cmd}'[:4] == 'key:':
|
||||
ret = await self.async_send_cloud_command(did, cmd)
|
||||
elif f'{cmd}'.startswith('FE'):
|
||||
ret = await self.miot_device.async_send('send_ir_code', [cmd])
|
||||
else:
|
||||
ret = self.hass.async_add_executor_job(self.miot_device.play, cmd)
|
||||
self.logger.info('%s: Send IR command %s(%s) result: %s', self.name_model, cmd, kwargs, ret)
|
||||
except (DeviceException, MiCloudException) as exc:
|
||||
self.logger.error('%s: Send IR command %s(%s) failed: %s', self.name_model, cmd, kwargs, exc)
|
||||
await asyncio.sleep(delays)
|
||||
|
||||
async def async_send_cloud_command(self, did, command):
|
||||
key = f'{command}'
|
||||
if key[:4] == 'key:':
|
||||
key = key[4:]
|
||||
try:
|
||||
key = int(key)
|
||||
except (TypeError, ValueError):
|
||||
key = None
|
||||
if not did or not key:
|
||||
self.logger.warning('%s: IR command %s to %s invalid for cloud.', self.name_model, command, did)
|
||||
return False
|
||||
mic = self.miot_cloud
|
||||
if not mic:
|
||||
return False
|
||||
res = await mic.async_request_api('v2/irdevice/controller/key/click', {
|
||||
'did': did,
|
||||
'key_id': key,
|
||||
}) or {}
|
||||
if res.get('code'):
|
||||
self.logger.warning('%s: Send IR command %s(%s) failed: %s', self.name_model, command, did, res)
|
||||
return res
|
||||
|
||||
async def async_learn_command(self, **kwargs):
|
||||
"""Learn a command from a device."""
|
||||
if not self.miot_device:
|
||||
return {'error': f'Not support for {self.model}'}
|
||||
timeout = int(kwargs.get(remote.ATTR_TIMEOUT) or 30)
|
||||
res = {}
|
||||
key = int(kwargs.get(remote.ATTR_DEVICE, 999999))
|
||||
try:
|
||||
for idx in range(timeout):
|
||||
if hasattr(self.miot_device, 'read'):
|
||||
learn = self.hass.async_add_executor_job(self.miot_device.learn, key)
|
||||
read = self.hass.async_add_executor_job(self.miot_device.read, key)
|
||||
else:
|
||||
learn = self.miot_device.async_send('start_ir_learn', [key])
|
||||
read = self.miot_device.async_send('get_ir_learn_result')
|
||||
if idx == 0:
|
||||
await learn
|
||||
await asyncio.sleep(1)
|
||||
res = await read
|
||||
if isinstance(res, dict) and res.get('code'):
|
||||
break
|
||||
if isinstance(res, list) and res and str(res[0]).startswith('FE'):
|
||||
# lumi.acpartner.v*
|
||||
res = {'result': res}
|
||||
break
|
||||
except Exception as exc:
|
||||
res = {'error': f'{exc}'}
|
||||
self.logger.warning(
|
||||
'%s: Learn command failed, the device ID must between 1 and 1000000. %s',
|
||||
self.name_model, exc,
|
||||
)
|
||||
persistent_notification.async_create(
|
||||
self.hass,
|
||||
f'{res}',
|
||||
'Remote learn result',
|
||||
f'{DOMAIN}-remote-learn',
|
||||
)
|
||||
return res
|
||||
|
||||
def delete_command(self, **kwargs):
|
||||
"""Delete commands from the database."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_press_ir_key(self, select, **kwargs):
|
||||
key = None
|
||||
did = kwargs.get('attr')
|
||||
for d in self._state_attrs.get('ir_devices', []):
|
||||
if did and did != d.get('did'):
|
||||
continue
|
||||
for k in d.get('keys', []):
|
||||
if select not in [
|
||||
k.get('display_name'),
|
||||
k.get('name'),
|
||||
self._translations.get(k.get('display_name') or k.get('name')),
|
||||
]:
|
||||
continue
|
||||
key = k.get('id')
|
||||
if key:
|
||||
return await self.async_send_cloud_command(did, key)
|
||||
return False
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
pwd
|
||||
url=$1
|
||||
mp4=${2:-latest.mp4}
|
||||
m3u8=$(curl -s "$url")
|
||||
KEY=$(echo "$m3u8" | grep EXT-X-KEY | cut -d'"' -f2 | xargs curl -sL | xxd -p -c 32)
|
||||
KIV=$(echo "$m3u8" | grep -oP 'IV=0x\w+' | sed 's/IV=0x//')
|
||||
whitelist="pipe,file,https,tls,tcp,crypto,concat"
|
||||
temp="temp_segs.txt"
|
||||
|
||||
i=0
|
||||
for line in $(echo "$m3u8" | grep -v '^#'); do
|
||||
echo "$i $line"
|
||||
seg="temp_$i.mp4"
|
||||
ffmpeg -loglevel warning -y \
|
||||
-protocol_whitelist "$whitelist" \
|
||||
-decryption_key "$KEY" -decryption_iv "$KIV" \
|
||||
-i "crypto+$line" \
|
||||
-c copy "$seg"
|
||||
if [ -f "$seg" ]; then
|
||||
echo "file '$seg'" >> $temp
|
||||
fi
|
||||
((i++))
|
||||
done
|
||||
|
||||
ffmpeg -loglevel warning -y \
|
||||
-protocol_whitelist "$whitelist" \
|
||||
-f concat -safe 0 \
|
||||
-i "$temp" \
|
||||
-c copy "$mp4"
|
||||
rm -vf temp_*
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Support select entity for Xiaomi Miot."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.select import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
SelectEntity as BaseEntity,
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
BaseSubEntity,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
|
||||
_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
|
||||
bind_services_to_entries(hass, SERVICE_TO_METHOD)
|
||||
|
||||
|
||||
class SelectEntity(XEntity, BaseEntity, RestoreEntity):
|
||||
_attr_current_option = None
|
||||
_attr_options = []
|
||||
|
||||
def on_init(self):
|
||||
if self._miot_property:
|
||||
self._attr_options = self._miot_property.list_descriptions()
|
||||
if self._miot_action:
|
||||
self._attr_options.insert(0, '')
|
||||
if lst := getattr(self.conv, 'options', None):
|
||||
self._attr_options = lst
|
||||
self._attr_available = True
|
||||
|
||||
def get_state(self) -> dict:
|
||||
return {self.attr: self._attr_current_option}
|
||||
|
||||
def set_state(self, data: dict):
|
||||
val = self.conv.value_from_dict(data)
|
||||
if val is None:
|
||||
return
|
||||
self._attr_current_option = val
|
||||
|
||||
async def async_select_option(self, option: str):
|
||||
if self._miot_action and option == '':
|
||||
return
|
||||
|
||||
await self.device.async_write({self.attr: option})
|
||||
|
||||
if self._miot_action:
|
||||
self._attr_current_option = ''
|
||||
async_call_later(self.hass, 0.5, self.schedule_update_ha_state)
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = SelectEntity
|
||||
|
||||
|
||||
class SelectSubEntity(BaseEntity, BaseSubEntity):
|
||||
def __init__(self, parent, attr, option=None):
|
||||
BaseSubEntity.__init__(self, parent, attr, option)
|
||||
self._available = True
|
||||
self._attr_current_option = None
|
||||
self._attr_options = self._option.get('options') or []
|
||||
self._select_option = self._option.get('select_option')
|
||||
self._async_select_option = self._option.get('async_select_option')
|
||||
|
||||
def update(self, data=None):
|
||||
super().update(data)
|
||||
self._attr_current_option = self._attr_state
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def select_option(self, option):
|
||||
"""Change the selected option."""
|
||||
if self._select_option:
|
||||
kws = {
|
||||
'attr': self._attr,
|
||||
'option': self._option,
|
||||
}
|
||||
if ret := self._select_option(option, **kws):
|
||||
self._attr_current_option = option
|
||||
self.schedule_update_ha_state()
|
||||
return ret
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_select_option(self, option: str):
|
||||
"""Change the selected option."""
|
||||
if self._async_select_option:
|
||||
kws = {
|
||||
'attr': self._attr,
|
||||
'option': self._option,
|
||||
}
|
||||
if ret := await self._async_select_option(option, **kws):
|
||||
self._attr_current_option = option
|
||||
self.schedule_update_ha_state()
|
||||
return ret
|
||||
raise NotImplementedError()
|
||||
|
||||
def update_options(self, options: list):
|
||||
self._attr_options = options
|
||||
@@ -0,0 +1,636 @@
|
||||
"""Support for Xiaomi sensors."""
|
||||
import logging
|
||||
import time
|
||||
import json
|
||||
from typing import cast
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cmp_to_key, cached_property
|
||||
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
SensorEntity as BaseEntity,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity, RestoredExtraData
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
CONF_MODEL,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
MiotEntity,
|
||||
BaseSubEntity,
|
||||
MiCoordinatorEntity,
|
||||
MiotCloud,
|
||||
async_setup_config_entry,
|
||||
bind_services_to_entries,
|
||||
)
|
||||
from .core.miot_spec import (
|
||||
MiotSpec,
|
||||
MiotService,
|
||||
)
|
||||
from .core.utils import local_zone, get_translation
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
|
||||
SERVICE_TO_METHOD = {}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
entry = HassEntry.init(hass, config_entry).new_adder(ENTITY_DOMAIN, async_add_entities)
|
||||
cloud = await entry.get_cloud()
|
||||
|
||||
if cloud:
|
||||
if not entry.get_config('disable_message'):
|
||||
hass.data[DOMAIN]['accounts'].setdefault(cloud.user_id, {})
|
||||
|
||||
if not hass.data[DOMAIN]['accounts'][cloud.user_id].get('messenger'):
|
||||
entity = MihomeMessageSensor(hass, cloud)
|
||||
await entity.coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN]['accounts'][cloud.user_id]['messenger'] = entity
|
||||
async_add_entities([entity], update_before_add=False)
|
||||
|
||||
if not entry.get_config('disable_scene_history'):
|
||||
homes = await cloud.async_get_homerooms()
|
||||
for home in homes:
|
||||
home_id = home.get('id')
|
||||
if hass.data[DOMAIN]['accounts'][cloud.user_id].get(f'scene_history_{home_id}'):
|
||||
continue
|
||||
|
||||
entity = MihomeSceneHistorySensor(hass, cloud, home_id, home.get('uid'))
|
||||
await entity.coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN]['accounts'][cloud.user_id][f'scene_history_{home_id}'] = entity
|
||||
async_add_entities([entity], update_before_add=False)
|
||||
|
||||
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
|
||||
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(
|
||||
'switch_sensor',
|
||||
'lock', 'door', 'bed', 'walking_pad', 'treadmill',
|
||||
'fridge_chamber', 'germicidal_lamp', 'vital_signs',
|
||||
'sterilizer', 'steriliser', 'chair', 'dryer', 'clothes_dryer',
|
||||
):
|
||||
if srv.name in ['lock']:
|
||||
if not srv.get_property('operation_method', 'operation_id'):
|
||||
continue
|
||||
elif srv.name in ['door']:
|
||||
if spec.get_service('lock'):
|
||||
continue
|
||||
elif not srv.mapping():
|
||||
continue
|
||||
entities.append(MiotSensorEntity(config, srv))
|
||||
for entity in entities:
|
||||
hass.data[DOMAIN]['entities'][entity.unique_id] = entity
|
||||
async_add_entities(entities, update_before_add=False)
|
||||
bind_services_to_entries(hass, SERVICE_TO_METHOD)
|
||||
|
||||
|
||||
def datetime_with_tzinfo(value):
|
||||
if isinstance(value, datetime):
|
||||
pass
|
||||
elif isinstance(value, str):
|
||||
value = datetime.fromisoformat(value)
|
||||
elif isinstance(value, (int, float)):
|
||||
value = datetime.fromtimestamp(value)
|
||||
else:
|
||||
value = cast(datetime, value)
|
||||
if value.tzinfo is None:
|
||||
value = value.replace(tzinfo=local_zone())
|
||||
return value
|
||||
|
||||
|
||||
class SensorEntity(XEntity, BaseEntity, RestoreEntity):
|
||||
def on_init(self):
|
||||
self._attr_state_class = self.custom_config('state_class')
|
||||
self._attr_native_unit_of_measurement = self.custom_config('unit_of_measurement')
|
||||
if self._miot_property:
|
||||
if not self._attr_icon:
|
||||
self._attr_icon = self._miot_property.entity_icon
|
||||
if not self._attr_device_class:
|
||||
self._attr_device_class = self._miot_property.device_class
|
||||
if not self._attr_state_class:
|
||||
self._attr_state_class = self._miot_property.state_class
|
||||
if not self._attr_native_unit_of_measurement:
|
||||
self._attr_native_unit_of_measurement = self._miot_property.unit_of_measurement
|
||||
|
||||
def get_state(self) -> dict:
|
||||
return {self.attr: self._attr_native_value}
|
||||
|
||||
def set_state(self, data: dict):
|
||||
value = self.conv.value_from_dict(data)
|
||||
prop = self._miot_property
|
||||
if prop and prop.value_range:
|
||||
if not prop.range_valid(value):
|
||||
value = None
|
||||
if value is not None:
|
||||
try:
|
||||
if ratio := self.custom_value_ratio:
|
||||
value = round(float(value) * ratio, 3)
|
||||
elif self.state_class:
|
||||
value = round(float(value), 3)
|
||||
except (TypeError, ValueError):
|
||||
value = None
|
||||
if self.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
value = datetime_with_tzinfo(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@cached_property
|
||||
def custom_value_ratio(self):
|
||||
return self.custom_config_number('value_ratio') or 0
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = SensorEntity
|
||||
|
||||
|
||||
class MiotSensorEntity(MiotEntity, BaseEntity):
|
||||
|
||||
def __init__(self, config, miot_service: MiotService):
|
||||
super().__init__(miot_service, config=config, logger=_LOGGER)
|
||||
|
||||
first_property = None
|
||||
if len(miot_service.properties) > 0:
|
||||
first_property = list(miot_service.properties.values() or [])[0].name
|
||||
self._prop_state = miot_service.get_property(
|
||||
'status', 'fault', first_property or 'status',
|
||||
)
|
||||
if miot_service.name in ['lock']:
|
||||
self._prop_state = miot_service.get_property('operation_method') or self._prop_state
|
||||
|
||||
self._attr_icon = self._miot_service.entity_icon
|
||||
self._attr_state_class = None
|
||||
self._attr_native_unit_of_measurement = None
|
||||
if self._prop_state:
|
||||
self._name = f'{self.device_name} {self._prop_state.friendly_desc}'
|
||||
self._attr_icon = self._prop_state.entity_icon or self._attr_icon
|
||||
self._attr_state_class = self._prop_state.state_class
|
||||
|
||||
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
|
||||
if self._prop_state:
|
||||
self._attr_icon = self._prop_state.entity_icon
|
||||
self._attr_device_class = self._prop_state.device_class
|
||||
self._attr_native_unit_of_measurement = self._prop_state.unit_of_measurement
|
||||
self._state_attrs.update({
|
||||
'state_property': self._prop_state.full_name,
|
||||
})
|
||||
|
||||
cls = self.custom_config('state_class')
|
||||
if cls and cls in SensorStateClass:
|
||||
self._attr_state_class = SensorStateClass(cls)
|
||||
elif cls in ['', False]:
|
||||
self._attr_state_class = None
|
||||
|
||||
if uom := self.custom_config('unit_of_measurement'):
|
||||
self._attr_native_unit_of_measurement = uom
|
||||
|
||||
async def async_update(self):
|
||||
await super().async_update()
|
||||
if not self._available or not self._prop_state:
|
||||
return
|
||||
if self._miot_service.name in ['lock'] and self._prop_state.full_name not in self._state_attrs:
|
||||
if how := self._state_attrs.get('lock_method'):
|
||||
await self.async_update_attrs({
|
||||
self._prop_state.full_name: get_translation(how, ['lock_method']),
|
||||
})
|
||||
elif edt := self._state_attrs.get('event.11', {}):
|
||||
if isinstance(edt, dict):
|
||||
await self.async_update_attrs({
|
||||
self._prop_state.full_name: edt.get('method'),
|
||||
})
|
||||
self._prop_state.description_to_dict(self._state_attrs)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this entity."""
|
||||
return self.get_device_class(SensorDeviceClass)
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
prop = self._prop_state
|
||||
if not prop:
|
||||
return None
|
||||
key = f'{prop.full_name}_desc'
|
||||
if key in self._state_attrs:
|
||||
return f'{self._state_attrs[key]}'.lower()
|
||||
val = prop.from_device(self.device)
|
||||
if not prop.range_valid(val):
|
||||
val = None
|
||||
return val
|
||||
|
||||
|
||||
class BaseSensorSubEntity(BaseSubEntity, BaseEntity):
|
||||
def __init__(self, parent, attr, option=None, **kwargs):
|
||||
kwargs.setdefault('domain', ENTITY_DOMAIN)
|
||||
self._attr_state_class = None
|
||||
super().__init__(parent, attr, option, **kwargs)
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
value = self._attr_state
|
||||
if hasattr(self, '_attr_native_value') and self._attr_native_value is not None:
|
||||
value = self._attr_native_value
|
||||
value = get_translation(value, [self._attr])
|
||||
if self.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
value = datetime_with_tzinfo(value)
|
||||
return value
|
||||
|
||||
@property
|
||||
def state_class(self):
|
||||
return self._attr_state_class
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
cls = self.custom_config('state_class')
|
||||
if cls and cls in SensorStateClass:
|
||||
self._attr_state_class = SensorStateClass(cls)
|
||||
elif cls in ['', False]:
|
||||
self._attr_state_class = None
|
||||
|
||||
if uom := self.custom_config('unit_of_measurement'):
|
||||
self._attr_native_unit_of_measurement = uom
|
||||
|
||||
def update(self, data=None):
|
||||
old_state = self._attr_state
|
||||
super().update(data)
|
||||
|
||||
now = datetime.now(tz=local_zone(self.hass))
|
||||
if self.state_class in ['total_increasing'] and old_state not in [None, '', STATE_UNKNOWN]:
|
||||
ptm = self._extra_attrs.get('updated_time') or now
|
||||
if now.strftime('%Y-%m-%d') == ptm.strftime('%Y-%m-%d'):
|
||||
try:
|
||||
if (self._attr_state or 0) < old_state:
|
||||
self._attr_state = old_state
|
||||
except (TypeError, ValueError) as exc:
|
||||
_LOGGER.warning(
|
||||
'%s: Total increasing sensor state error: %s',
|
||||
self.name_model, [exc, self._attr_state, old_state],
|
||||
)
|
||||
if self._attr_state != old_state:
|
||||
self._extra_attrs['updated_time'] = now
|
||||
|
||||
|
||||
class MihomeMessageSensor(MiCoordinatorEntity, BaseEntity, RestoreEntity):
|
||||
_filter_homes = None
|
||||
_exclude_types = None
|
||||
_has_none_message = False
|
||||
|
||||
def __init__(self, hass, cloud: MiotCloud):
|
||||
self.hass = hass
|
||||
self.cloud = cloud
|
||||
self.message = {}
|
||||
self.entity_id = f'{ENTITY_DOMAIN}.mi_{cloud.user_id}_message'
|
||||
self._attr_unique_id = f'{DOMAIN}-mihome-message-{cloud.user_id}'
|
||||
self._attr_name = f'Xiaomi {cloud.user_id} message'
|
||||
self._attr_icon = 'mdi:message'
|
||||
self._attr_should_poll = False
|
||||
self._attr_native_value = None
|
||||
self._attr_extra_state_attributes = {
|
||||
'entity_class': self.__class__.__name__,
|
||||
}
|
||||
self.coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=self._attr_unique_id,
|
||||
update_method=self.fetch_latest_message,
|
||||
update_interval=timedelta(seconds=15),
|
||||
)
|
||||
super().__init__(self.coordinator)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
|
||||
self._filter_homes = self.custom_config_list('filter_home') or []
|
||||
self._exclude_types = list(map(lambda x: int(x), self.custom_config_list('exclude_type', [13]) or []))
|
||||
if sec := self.custom_config_integer('interval_seconds'):
|
||||
self.coordinator.update_interval = timedelta(seconds=sec)
|
||||
|
||||
if restored := await self.async_get_last_extra_data():
|
||||
self._attr_native_value = restored.as_dict().get('state')
|
||||
self._attr_extra_state_attributes.update(restored.as_dict().get('attrs', {}))
|
||||
|
||||
self._attr_extra_state_attributes['filter_homes'] = self._filter_homes
|
||||
self._attr_extra_state_attributes['exclude_types'] = self._exclude_types
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Run when entity will be removed from hass.
|
||||
To be extended by integrations.
|
||||
"""
|
||||
await super().async_will_remove_from_hass()
|
||||
self.hass.data[DOMAIN]['accounts'].get(self.cloud.user_id, {}).pop('messenger', None)
|
||||
|
||||
@property
|
||||
def extra_restore_state_data(self):
|
||||
"""Return entity specific state data to be restored."""
|
||||
return RestoredExtraData({
|
||||
'state': self.native_value,
|
||||
'attrs': self._attr_extra_state_attributes,
|
||||
})
|
||||
|
||||
async def async_set_message(self, msg):
|
||||
if msg == self.message:
|
||||
return
|
||||
if old := self._attr_native_value:
|
||||
self._attr_extra_state_attributes['prev_message'] = old
|
||||
con = msg.get('content')
|
||||
if tit := msg.get('title'):
|
||||
if con:
|
||||
self._attr_native_value = f'{con}: {tit}'
|
||||
else:
|
||||
self._attr_native_value = tit
|
||||
logger = _LOGGER.info if old != self._attr_native_value else _LOGGER.debug
|
||||
logger('New xiaomi message for %s: %s', self.cloud.user_id, self._attr_native_value)
|
||||
tim = msg.get('ctime')
|
||||
params = msg.get('params', {})
|
||||
body = params.get('body', {})
|
||||
self._attr_entity_picture = msg.get('img_url') or None
|
||||
self._attr_extra_state_attributes.update({
|
||||
'msg_id': msg.get('msg_id'),
|
||||
'is_new': msg.get('is_new'),
|
||||
'type': msg.get('type'),
|
||||
'title': tit,
|
||||
'content': con,
|
||||
'user_id': msg.get('uid'),
|
||||
'ctime': tim,
|
||||
'timestamp': datetime.fromtimestamp(tim, local_zone()) if tim else None,
|
||||
'model': params.get('model', body.get('model')),
|
||||
'device_id': msg.get('did', body.get('did')),
|
||||
'home_name': msg.get('homeName'),
|
||||
'room_name': msg.get('roomName'),
|
||||
'event': body.get('event'),
|
||||
'event_data': body.get('extra', body.get('value')),
|
||||
})
|
||||
|
||||
async def fetch_latest_message(self):
|
||||
res = await self.cloud.async_request_api('v2/message/v2/typelist', data={}) or {}
|
||||
mls = (res.get('result') or {}).get('messages') or []
|
||||
mls.sort(key=lambda x: x.get('ctime', 0), reverse=False)
|
||||
prev_time = self._attr_extra_state_attributes.get('ctime')
|
||||
prev_mid = self._attr_extra_state_attributes.get('msg_id')
|
||||
msg = {}
|
||||
for m in mls:
|
||||
hre = m.get('params', {}).get('body', {}).get('homeRoomExtra', {})
|
||||
home = hre.get('homeName')
|
||||
if self._filter_homes and home and home not in self._filter_homes:
|
||||
continue
|
||||
typ = m.get('type', 0)
|
||||
if self._exclude_types and typ in self._exclude_types:
|
||||
continue
|
||||
tim = m.get('ctime', 0)
|
||||
mid = m.get('msg_id', 0)
|
||||
if prev_time and tim < prev_time:
|
||||
continue
|
||||
if prev_mid and mid <= prev_mid:
|
||||
continue
|
||||
m['homeName'] = home
|
||||
m['roomName'] = hre.get('roomName')
|
||||
msg = m
|
||||
break
|
||||
if not mls:
|
||||
if not self._has_none_message:
|
||||
# Only raise a warning if there was a failure obtaining the xiaomi message
|
||||
# Otherwise, a warning will show anytime that there are simply no messages waiting
|
||||
code = res.get('code')
|
||||
message = res.get('message')
|
||||
if code == 0 and message == 'ok':
|
||||
_LOGGER.debug('Get xiaomi message for %s failed: %s', self.cloud.user_id, res)
|
||||
else:
|
||||
_LOGGER.warning('Get xiaomi message for %s failed: %s', self.cloud.user_id, res)
|
||||
self._has_none_message = True
|
||||
if msg:
|
||||
await self.async_set_message(msg)
|
||||
self.message = msg
|
||||
self._has_none_message = False
|
||||
return msg
|
||||
|
||||
|
||||
class MihomeSceneHistorySensor(MiCoordinatorEntity, BaseEntity, RestoreEntity):
|
||||
MESSAGE_TIMEOUT = 60
|
||||
UPDATE_INTERVAL = 15
|
||||
|
||||
_has_none_message = False
|
||||
|
||||
def __init__(self, hass, cloud: MiotCloud, home_id, owner_user_id):
|
||||
self.hass = hass
|
||||
self.cloud = cloud
|
||||
self.home_id = int(home_id)
|
||||
self.owner_user_id = int(owner_user_id)
|
||||
self.entity_id = f'{ENTITY_DOMAIN}.mi_{cloud.user_id}_{home_id}_scene_history'
|
||||
self._attr_unique_id = f'{DOMAIN}-mihome-scene-history-{cloud.user_id}_{home_id}'
|
||||
self._attr_name = f'Xiaomi {cloud.user_id}_{home_id} Scene History'
|
||||
self._attr_icon = 'mdi:message'
|
||||
self._attr_should_poll = False
|
||||
self._attr_native_value = None
|
||||
self._attr_extra_state_attributes = {
|
||||
'entity_class': self.__class__.__name__,
|
||||
}
|
||||
self.coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=self._attr_unique_id,
|
||||
update_method=self.fetch_latest_message,
|
||||
update_interval=timedelta(seconds=self.UPDATE_INTERVAL),
|
||||
)
|
||||
super().__init__(self.coordinator)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
|
||||
if sec := self.custom_config_integer('interval_seconds'):
|
||||
self.coordinator.update_interval = timedelta(seconds=sec)
|
||||
|
||||
if restored := await self.async_get_last_extra_data():
|
||||
restored_dict = restored.as_dict()
|
||||
|
||||
attrs = restored_dict.get('attrs', {})
|
||||
if ts := attrs.get('ts'):
|
||||
attrs['timestamp'] = datetime.fromtimestamp(ts, local_zone()) if ts else None
|
||||
|
||||
_LOGGER.debug(
|
||||
'xiaomi scene history %s %d, async_added_to_hass restore state: state= %s attrs= %s',
|
||||
self.cloud.user_id, self.home_id, restored_dict.get('state'), attrs,
|
||||
)
|
||||
self._attr_native_value = restored_dict.get('state')
|
||||
self._attr_extra_state_attributes.update(attrs)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Run when entity will be removed from hass.
|
||||
To be extended by integrations.
|
||||
"""
|
||||
await super().async_will_remove_from_hass()
|
||||
self.hass.data[DOMAIN]['accounts'].get(self.cloud.user_id, {}).pop(f'scene_history_{self.home_id}', None)
|
||||
|
||||
@property
|
||||
def extra_restore_state_data(self):
|
||||
"""Return entity specific state data to be restored."""
|
||||
return RestoredExtraData({
|
||||
'state': self.native_value,
|
||||
'attrs': self._attr_extra_state_attributes,
|
||||
})
|
||||
|
||||
def trim_message(self, msg):
|
||||
ts = msg.get('time') or int(time.time())
|
||||
return {
|
||||
"from": msg.get('from'),
|
||||
"name": msg.get('name'),
|
||||
"ts": ts,
|
||||
'timestamp': datetime.fromtimestamp(ts, local_zone()),
|
||||
"scene_id": str(msg.get('userSceneId')),
|
||||
"targets": msg.get('msg', []),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _cmp_message(a, b):
|
||||
a_ts, b_ts = a.get('ts', 0), b.get('ts', 0)
|
||||
if a_ts != b_ts:
|
||||
return a_ts - b_ts
|
||||
|
||||
a_scene_id, b_scene_id = a.get('scene_id', 0), b.get('scene_id', 0)
|
||||
if a_scene_id < b_scene_id:
|
||||
return -1
|
||||
if a_scene_id > b_scene_id:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
async def async_set_message(self, msg):
|
||||
self._attr_native_value = msg.get('name')
|
||||
_LOGGER.debug('New xiaomi scene history for %s %d: %s', self.cloud.user_id, self.home_id, self._attr_native_value)
|
||||
|
||||
old = self._attr_extra_state_attributes or {}
|
||||
self._attr_extra_state_attributes.update({**msg, 'prev_value': old.get('name'), 'prev_scene_id': old.get('scene_id')})
|
||||
|
||||
async def fetch_latest_message(self):
|
||||
res = await self.cloud.async_request_api('scene/history', data={
|
||||
"home_id": self.home_id,
|
||||
"uid": int(self.cloud.user_id),
|
||||
"owner_uid": self.owner_user_id,
|
||||
"command": "history",
|
||||
"limit": 15,
|
||||
}) or {}
|
||||
|
||||
messages = [self.trim_message(msg) for msg in (res.get('result') or {}).get('history') or []]
|
||||
if not messages:
|
||||
if not self._has_none_message:
|
||||
_LOGGER.info('Get xiaomi scene history for %s %d failed: %s', self.cloud.user_id, self.home_id, res)
|
||||
|
||||
self._has_none_message = True
|
||||
return {}
|
||||
|
||||
messages.sort(key=cmp_to_key(self._cmp_message), reverse=False)
|
||||
_LOGGER.debug(
|
||||
'Get xiaomi scene history for %s %d success: prev_timestamp= %d prev_scene_id= %s messages= %s,',
|
||||
self.cloud.user_id, self.home_id,
|
||||
self._attr_extra_state_attributes.get('ts') or 0,
|
||||
self._attr_extra_state_attributes.get('scene_id') or '',
|
||||
messages,
|
||||
)
|
||||
|
||||
must_after = int(time.time()) - self.MESSAGE_TIMEOUT
|
||||
for msg in messages:
|
||||
if msg.get('ts') < must_after:
|
||||
continue
|
||||
|
||||
if self._cmp_message(msg, self._attr_extra_state_attributes) <= 0:
|
||||
continue
|
||||
|
||||
await self.async_set_message(msg)
|
||||
self._has_none_message = False
|
||||
return msg
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
class XiaoaiConversationSensor(MiCoordinatorEntity, BaseSensorSubEntity):
|
||||
def __init__(self, parent, hass, option=None):
|
||||
BaseSensorSubEntity.__init__(self, parent, 'conversation', option)
|
||||
self.hass = hass
|
||||
self.conversation = {}
|
||||
self._available = True
|
||||
self._attr_native_value = None
|
||||
self._option.setdefault('icon', 'mdi:account-voice')
|
||||
self.coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=self.unique_id,
|
||||
update_method=self.fetch_latest_message,
|
||||
update_interval=timedelta(seconds=5),
|
||||
)
|
||||
super().__init__(self.coordinator)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
await super().async_added_to_hass()
|
||||
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
|
||||
await self.coordinator.async_refresh()
|
||||
if sec := self.custom_config_integer('interval_seconds'):
|
||||
self.coordinator.update_interval = timedelta(seconds=sec)
|
||||
|
||||
async def fetch_latest_message(self):
|
||||
mic = self._parent.xiaoai_cloud
|
||||
dvc = self._parent.xiaoai_device or {}
|
||||
aid = dvc.get('deviceID')
|
||||
if not isinstance(mic, MiotCloud) or not aid:
|
||||
self._available = False
|
||||
return
|
||||
api = 'https://userprofile.mina.mi.com/device_profile/v2/conversation'
|
||||
dat = {
|
||||
'hardware': dvc.get('hardware', ''),
|
||||
'timestamp': int(time.time() * 1000),
|
||||
'limit': 3,
|
||||
}
|
||||
cks = {
|
||||
'deviceId': aid,
|
||||
}
|
||||
try:
|
||||
res = await mic.async_request_api(api, data=dat, method='GET', cookies=cks) or {}
|
||||
rdt = res.get('data', {})
|
||||
if not isinstance(rdt, dict):
|
||||
rdt = json.loads(rdt) or {}
|
||||
except (TypeError, ValueError, Exception) as exc:
|
||||
rdt = {}
|
||||
_LOGGER.warning(
|
||||
'%s: Got exception while fetch xiaoai conversation: %s',
|
||||
self.name_model, [aid, exc],
|
||||
)
|
||||
mls = rdt.get('records') or []
|
||||
msg = mls.pop(0) if mls else {}
|
||||
self.conversation = msg
|
||||
old = self._attr_native_value
|
||||
if con := msg.get('query'):
|
||||
self._state = con
|
||||
self._attr_native_value = con
|
||||
logger = _LOGGER.info if old != self._attr_native_value else _LOGGER.debug
|
||||
logger('%s: New xiaoai conversation: %s', self.name_model, self._attr_native_value)
|
||||
tim = msg.get('time')
|
||||
ans = []
|
||||
for v in msg.get('answers', []):
|
||||
if not isinstance(v, dict):
|
||||
continue
|
||||
typ = v.get('type', '').lower()
|
||||
v.pop('bitSet', None)
|
||||
v.get(typ, {}).pop('bitSet', None)
|
||||
ans.append(v)
|
||||
self._state_attrs.update({
|
||||
'content': con,
|
||||
'answers': ans,
|
||||
'history': [
|
||||
v.get('query')
|
||||
for v in mls
|
||||
],
|
||||
'timestamp': datetime.fromtimestamp(tim / 1000, local_zone()) if tim else None,
|
||||
})
|
||||
return msg
|
||||
@@ -0,0 +1,333 @@
|
||||
send_command:
|
||||
description: Send miio command.
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: light.yeelight_living_e92c
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
method:
|
||||
description: Method of the command.
|
||||
example: set_power
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
params:
|
||||
description: Params for the method.
|
||||
example: ['on']
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
|
||||
get_properties:
|
||||
description: Get miot properties.
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: climate.xiaomi_mc5_374e
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
mapping:
|
||||
description: Mapping for properties.
|
||||
example: '[{"siid": 2, "piid": 1}, {"siid": 3, "piid": 1}]'
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
update_entity:
|
||||
description: Update to entity state attributes.
|
||||
default: false
|
||||
example: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
set_property:
|
||||
description: Set miot property.
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: climate.xiaomi_mc5_374e
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
field:
|
||||
description: Field of property.
|
||||
example: air_conditioner.on
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
value:
|
||||
description: Value of property.
|
||||
example: true
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
|
||||
set_miot_property:
|
||||
description: Set miot property by siid/piid.
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: climate.xiaomi_mc5_374e
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
siid:
|
||||
description: Miot service ID.
|
||||
example: 2
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
piid:
|
||||
description: Miot property ID.
|
||||
example: 1
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
value:
|
||||
description: Miot property value.
|
||||
example: true
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
|
||||
call_action:
|
||||
description: Call miot action.
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: media_player.xiaoai_x08c
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
siid:
|
||||
description: Miot service ID.
|
||||
example: 3
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
aiid:
|
||||
description: Miot action ID.
|
||||
example: 5
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9999
|
||||
mode: box
|
||||
params:
|
||||
description: Miot action params.
|
||||
example: "['Turn on light', 1]"
|
||||
selector:
|
||||
object:
|
||||
|
||||
intelligent_speaker:
|
||||
description: Play text on Xiaoai speaker / Execute text directive / 小爱TTS及执行语音命令
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: media_player.xiaoai_x08c
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
domain: media_player
|
||||
text:
|
||||
description: Text content.
|
||||
example: Turn on light
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
execute:
|
||||
description: Execute text directive.
|
||||
default: false
|
||||
example: true
|
||||
selector:
|
||||
boolean:
|
||||
silent:
|
||||
description: Silent execution.
|
||||
default: false
|
||||
example: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
xiaoai_wakeup:
|
||||
description: Wake up the Xiaoai speaker / 唤醒小爱音箱
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the xiaoai entity.
|
||||
example: media_player.xiaoai_x08c
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
domain: media_player
|
||||
text:
|
||||
description: Text content for wake up.
|
||||
example: Livingroom light
|
||||
selector:
|
||||
text:
|
||||
|
||||
get_device_data:
|
||||
description: Get xiaomi device data from cloud.
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: sensor.xiaomi_lock
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
type:
|
||||
description: Data type.
|
||||
default: prop
|
||||
example: prop
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- prop
|
||||
- event
|
||||
- prop_cal_day
|
||||
- prop_cal_week
|
||||
- prop_cal_month
|
||||
- store
|
||||
key:
|
||||
description: Data key.
|
||||
example: power
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
time_start:
|
||||
description: From the unix timestamp.
|
||||
example: 0
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 9999999999
|
||||
mode: box
|
||||
time_end:
|
||||
description: To the unix timestamp.
|
||||
example: 0
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 9999999999
|
||||
mode: box
|
||||
limit:
|
||||
description: Limit of results.
|
||||
example: 10
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 2000
|
||||
mode: box
|
||||
group:
|
||||
description: Results grouping.
|
||||
default: raw
|
||||
example: raw
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- raw
|
||||
- hour
|
||||
- day
|
||||
- week
|
||||
- month
|
||||
|
||||
renew_devices:
|
||||
description: Renew xiaomi devices.
|
||||
fields:
|
||||
username:
|
||||
description: Xiaomi Account ID / Email / Phone.
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
|
||||
get_token:
|
||||
description: Get xiaomi device token.
|
||||
fields:
|
||||
name:
|
||||
description: Keyword of device name in Mihome / IP / Model.
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
|
||||
get_bindkey:
|
||||
description: Get bindkey for ble device from cloud.
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: sensor.lywsd03mmc_temperature
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
did:
|
||||
description: Xiaomi device ID.
|
||||
selector:
|
||||
text:
|
||||
|
||||
request_xiaomi_api:
|
||||
description: Request xiaomi cloud api.
|
||||
fields:
|
||||
entity_id:
|
||||
description: ID of the entity.
|
||||
example: sensor.xiaomi_device
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: xiaomi_miot
|
||||
api:
|
||||
description: Xiaomi API path.
|
||||
example: /home/device_list
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
data:
|
||||
description: Xiaomi API request data.
|
||||
example: "{'getVirtualModel':true}"
|
||||
selector:
|
||||
object:
|
||||
method:
|
||||
description: Request method.
|
||||
default: POST
|
||||
example: POST
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- POST
|
||||
- GET
|
||||
crypt:
|
||||
description: Crypt data.
|
||||
default: true
|
||||
example: true
|
||||
selector:
|
||||
boolean:
|
||||
sid:
|
||||
description: Xiaomi service ID.
|
||||
default: xiaomiio
|
||||
example: xiaomiio
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: Mihome API
|
||||
value: xiaomiio
|
||||
- label: Xiaoai API
|
||||
value: micoapi
|
||||
- label: i.mi.com
|
||||
value: i.mi.com
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Support for Xiaomi switches."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
SwitchEntity as BaseEntity,
|
||||
SwitchDeviceClass,
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
MiotToggleEntity,
|
||||
ToggleSubEntity,
|
||||
async_setup_config_entry,
|
||||
)
|
||||
from .core.miot_spec import MiotService
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class SwitchEntity(XEntity, BaseEntity, RestoreEntity):
|
||||
def get_state(self) -> dict:
|
||||
return {self.attr: self._attr_is_on}
|
||||
|
||||
def set_state(self, data: dict):
|
||||
val = self.conv.value_from_dict(data)
|
||||
if val is None:
|
||||
return
|
||||
self._attr_is_on = bool(val)
|
||||
|
||||
async def async_turn_on(self):
|
||||
await self.device.async_write({self.attr: True})
|
||||
|
||||
async def async_turn_off(self):
|
||||
await self.device.async_write({self.attr: False})
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = SwitchEntity
|
||||
|
||||
|
||||
class MiotSwitchEntity(MiotToggleEntity, BaseEntity):
|
||||
def __init__(self, config: dict, miot_service: MiotService):
|
||||
super().__init__(miot_service, config=config, logger=_LOGGER)
|
||||
self._attr_icon = self._miot_service.entity_icon
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
if cls := self.get_device_class(SwitchDeviceClass):
|
||||
return cls
|
||||
typ = f'{self.model} {self._miot_service.spec.type}'
|
||||
if 'outlet' in typ or '.plug.' in typ:
|
||||
return SwitchDeviceClass.OUTLET
|
||||
return SwitchDeviceClass.SWITCH
|
||||
|
||||
|
||||
class SwitchSubEntity(ToggleSubEntity, BaseEntity):
|
||||
def __init__(self, parent, attr='switch', option=None, **kwargs):
|
||||
kwargs.setdefault('domain', ENTITY_DOMAIN)
|
||||
super().__init__(parent, attr, option, **kwargs)
|
||||
|
||||
def update(self, data=None):
|
||||
super().update(data)
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Provide info to system health."""
|
||||
from homeassistant.components import system_health
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .core.utils import async_get_manifest
|
||||
from .core.xiaomi_cloud import MiotCloud
|
||||
|
||||
|
||||
@callback
|
||||
def async_register(hass: HomeAssistant, register: system_health.SystemHealthRegistration) -> None:
|
||||
"""Register system health callbacks."""
|
||||
register.async_register_info(system_health_info, '/config/integrations')
|
||||
|
||||
|
||||
async def system_health_info(hass):
|
||||
"""Get info for the info page."""
|
||||
mic = None
|
||||
uas = {}
|
||||
uds = {}
|
||||
all_devices = {}
|
||||
for mic in MiotCloud.all_clouds(hass):
|
||||
uas[mic.user_id] = mic
|
||||
uds[mic.unique_id] = await mic.async_get_devices_by_key('did') or {}
|
||||
all_devices.update(uds[mic.unique_id])
|
||||
|
||||
api = mic.get_api_url('') if mic else 'https://api.io.mi.com'
|
||||
api_spec = 'https://miot-spec.org/miot-spec-v2/spec/services'
|
||||
|
||||
version = await async_get_manifest(hass, 'version', 'unknown')
|
||||
data = {
|
||||
'component_version': version,
|
||||
'can_reach_server': system_health.async_check_can_reach_url(hass, api),
|
||||
'can_reach_spec': system_health.async_check_can_reach_url(
|
||||
hass, api_spec, 'https://home.miot-spec.com/?cant-reach',
|
||||
),
|
||||
'logged_accounts': len(uas),
|
||||
'total_devices': len(all_devices),
|
||||
}
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Support text entity for Xiaomi Miot."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.text import (
|
||||
DOMAIN as ENTITY_DOMAIN,
|
||||
TextEntity as BaseEntity,
|
||||
)
|
||||
|
||||
from . import (
|
||||
DOMAIN,
|
||||
XIAOMI_CONFIG_SCHEMA as PLATFORM_SCHEMA, # noqa: F401
|
||||
HassEntry,
|
||||
XEntity,
|
||||
async_setup_config_entry,
|
||||
)
|
||||
|
||||
_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, {})
|
||||
|
||||
|
||||
class TextEntity(XEntity, BaseEntity):
|
||||
_attr_native_value = ''
|
||||
|
||||
def get_state(self) -> dict:
|
||||
return {self.attr: self._attr_native_value}
|
||||
|
||||
def set_state(self, data: dict):
|
||||
val = self.conv.value_from_dict(data)
|
||||
if val is None:
|
||||
return
|
||||
if isinstance(val, list):
|
||||
val = val[0] if val else None
|
||||
if val is None:
|
||||
val = ''
|
||||
self._attr_native_value = val
|
||||
|
||||
async def async_set_value(self, value: str):
|
||||
if self._miot_action and self._miot_action.name == 'execute_text_directive':
|
||||
silent = self.custom_config_integer('silent_execution', 0)
|
||||
silent_prop = self._miot_service.get_property('silent_execution')
|
||||
if silent_prop:
|
||||
"""
|
||||
xiaomi.wifispeaker.07g bool
|
||||
xiaomi.wifispeaker.16b bool
|
||||
xiaomi.wifispeaker.l04m 0:On 1:Off
|
||||
xiaomi.wifispeaker.l05b bool
|
||||
xiaomi.wifispeaker.l05c bool
|
||||
xiaomi.wifispeaker.l05g bool
|
||||
xiaomi.wifispeaker.l06a 0:On 1:Off
|
||||
xiaomi.wifispeaker.l09a 0:On 1:Off
|
||||
xiaomi.wifispeaker.l09b bool
|
||||
xiaomi.wifispeaker.l15a bool
|
||||
xiaomi.wifispeaker.l16a bool
|
||||
xiaomi.wifispeaker.l17a bool
|
||||
xiaomi.wifispeaker.l7a 0:On 1:Off
|
||||
xiaomi.wifispeaker.lx01 0:On 1:Off
|
||||
xiaomi.wifispeaker.lx04 0:On 1:Off
|
||||
xiaomi.wifispeaker.lx05 bool
|
||||
xiaomi.wifispeaker.lx06 bool
|
||||
xiaomi.wifispeaker.lx5a bool
|
||||
xiaomi.wifispeaker.m03a bool
|
||||
xiaomi.wifispeaker.s12 bool
|
||||
xiaomi.wifispeaker.x08a 0:On 1:Off
|
||||
xiaomi.wifispeaker.x08c 0:On 1:Off
|
||||
xiaomi.wifispeaker.x08e bool
|
||||
xiaomi.wifispeaker.x10a bool
|
||||
xiaomi.wifispeaker.x6a bool
|
||||
xiaomi.wifispeaker.x8f bool
|
||||
xiaomi.wifispeaker.x8s bool
|
||||
"""
|
||||
if silent_prop.value_list:
|
||||
val = silent_prop.list_value('On' if silent else 'Off')
|
||||
if val is None:
|
||||
val = 0 if silent else 1
|
||||
silent = val
|
||||
value = [value, silent]
|
||||
|
||||
await self.device.async_write({self.attr: value})
|
||||
|
||||
if self._miot_action:
|
||||
self._attr_native_value = ''
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
XEntity.CLS[ENTITY_DOMAIN] = TextEntity
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Vybrat akci",
|
||||
"data": {
|
||||
"action": "Akce"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Konfigurovat informace o zařízení.",
|
||||
"data": {
|
||||
"host": "Místní IP",
|
||||
"token": "Token",
|
||||
"name": "Název zařízení",
|
||||
"scan_interval": "Interval aktualizace stavu (v sekundách)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Konfigurace cloudového účtu Xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID účtu / e-mail Xiaomi",
|
||||
"password": "Heslo",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Umístění serveru MiCloud",
|
||||
"conn_mode": "Režim připojení zařízení",
|
||||
"filter_models": "Filtrace zařízení přes model/domácnost/WiFi (pokročilé)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtrovat cloudová zařízení",
|
||||
"description": "Můžete zahrnout, nebo vyloučit zařízení a integrovat pouze zařízení, která chcete.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Model filtrovaného zařízení",
|
||||
"model_list": "Seznam modelů zařízení",
|
||||
"filter_home_id": "Filtr Domů",
|
||||
"home_id_list": "Domácí seznam",
|
||||
"filter_ssid": "Filtr WiFi SSID",
|
||||
"ssid_list": "Seznam WiFi SSID",
|
||||
"filter_bssid": "Filtr WiFi BSSID",
|
||||
"bssid_list": "Seznam WiFi BSSID",
|
||||
"filter_did": "Filtrované zařízení",
|
||||
"did_list": "Seznam zařízení"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Přizpůsobení entit / zařízení",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Vybrat doménu entity",
|
||||
"entity": "Vybrat entitu",
|
||||
"model": "Vybrat model zařízení",
|
||||
"model_specified": "Zadaný model zařízení (volitelné)",
|
||||
"only_main_entity": "Pouze hlavní (nadřazené) entity",
|
||||
"reset_customizes": "Obnovit výchozí přizpůsobení",
|
||||
"bool2selects": "Přizpůsobte možnosti"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Připojení zařízení se nezdařilo",
|
||||
"cannot_login": "Přihlášení k účtu Mi se nezdařilo, zkontrolujte prosím heslo účtu. Pokud se přihlášení po několika pokusech nezdaří, použijte k přihlášení na oficiální webové stránky Xiaomi (mi.com) jiné zařízení ve stejné síti LAN jako hostitel HA a zkuste to znovu.",
|
||||
"cannot_reach": "Nelze získat přístup k Xiaomi API",
|
||||
"tzinfo_error": "Viz: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Žádné zařízení v cloudovém účtu Xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Konfigurace uložena! [Přizpůsobit znovu](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nNěkteré přizpůsobitelné možnosti vyžadují [obnovení integrace](/developer-tools/yaml), aby se projevily.",
|
||||
"already_configured": "Zařízení je již nakonfigurováno",
|
||||
"not_xiaomi_miio": "Nejedná se o zařízení Xiaomi miio/miot."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Konfigurovat informace o zařízení.",
|
||||
"data": {
|
||||
"host": "Místní IP",
|
||||
"token": "Token",
|
||||
"scan_interval": "Interval aktualizace stavu (v sekundách)",
|
||||
"miot_cloud": "Povolit miot cloud (konfigurace účtu v YAML)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Konfigurace cloudového účtu Xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID účtu / e-mail Xiaomi",
|
||||
"password": "Heslo",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Umístění serveru MiCloud",
|
||||
"conn_mode": "Režim připojení pro zařízení",
|
||||
"renew_devices": "Vynutit obnovení zařízení",
|
||||
"disable_message": "Deaktivovat senzor upozornění Mihome"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtrovat zařízení v cloudu",
|
||||
"description": "Můžete zahrnout nebo vyloučit zařízení a integrovat pouze ta, která chcete. Pokud v režimu vyloučení nezaškrtnete žádnou možnost v seznamu, je to ekvivalentní zahrnutí všech možností.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Model filtrovaného zařízení",
|
||||
"model_list": "Seznam modelů zařízení",
|
||||
"filter_home_id": "Filtr Domů",
|
||||
"home_id_list": "Domácí seznam",
|
||||
"filter_ssid": "Filtr WiFi SSID",
|
||||
"ssid_list": "Seznam WiFi SSID",
|
||||
"filter_bssid": "Filtr WiFi BSSID",
|
||||
"bssid_list": "Seznam WiFi BSSID",
|
||||
"filter_did": "Filtrovat zařízení",
|
||||
"did_list": "Seznam zařízení"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Připojení zařízení se nezdařilo",
|
||||
"cannot_login": "Přihlášení k účtu Mi se nezdařilo, zkontrolujte heslo účtu. ",
|
||||
"cannot_reach": "Nelze získat přístup k Xiaomi API",
|
||||
"tzinfo_error": "Viz: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Žádné zařízení v cloudovém účtu Xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Verze komponenty",
|
||||
"can_reach_server": "Dosažení Xiaomi API Serveru",
|
||||
"can_reach_spec": "Dosažení MIoT-Spec Serveru",
|
||||
"logged_accounts": "Přihlášené účty",
|
||||
"total_devices": "Celkový počet zařízení MiHome"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Aktion wählen",
|
||||
"data": {
|
||||
"action": "Aktion"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Geräteinfo konfigurieren.",
|
||||
"data": {
|
||||
"host": "Lokale IP",
|
||||
"token": "Token",
|
||||
"name": "Gerätename",
|
||||
"scan_interval": "Status Update Interval (Sekunden)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Xiaomi Cloud Konto Konfigurieren.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Konto ID / Email",
|
||||
"password": "Passwort",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Server Standort der MiCloud",
|
||||
"conn_mode": "Verbindungsmodus für Gerät",
|
||||
"filter_models": "Geräte filern nach Modell/Zuhause/WiFi (Erweitert)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Cloud-Geräte filtern",
|
||||
"description": "Sie können Geräte ein- oder ausschließen, um nur die gewünschten Geräte zu integrieren. Wenn Sie im Ausschlussmodus keine Option in der Liste aktivieren, entspricht dies dem Einschließen aller Optionen.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Gerätemodell-Filter",
|
||||
"model_list": "Gerätemodell-Liste",
|
||||
"filter_home_id": "Zuhause Filtern",
|
||||
"home_id_list": "Home-Liste",
|
||||
"filter_ssid": "WiFi SSID filtern",
|
||||
"ssid_list": "WiFi SSID Liste",
|
||||
"filter_bssid": "WiFi BSSID filtern",
|
||||
"bssid_list": "WiFi BSSID Liste",
|
||||
"filter_did": "Gerätefilter",
|
||||
"did_list": "Geräte-Liste"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Entitäten / Geräte anpassen",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Entitäts-Domain wählen",
|
||||
"entity": "Entität wählen",
|
||||
"model": "Gerätemodell wählen",
|
||||
"model_specified": "Gerätemodell spezifizieren (Optional)",
|
||||
"only_main_entity": "Nur Haupt-Entitäten (parent)",
|
||||
"reset_customizes": "Zurücksetzen auf Standardanpassungen",
|
||||
"bool2selects": "Optionen anpassen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung zum Gerät fehlgeschlagen",
|
||||
"cannot_login": "Login in das Xiaomi Cloud-Konto fehlgeschlagen. Bitte überprüfe das Passwort. Wenn die Anmeldung nach mehreren Versuchen fehlschlägt, verwenden Sie bitte ein anderes Gerät im selben LAN wie der HA-Host, um sich auf der offiziellen Xiaomi-Website (mi.com) anzumelden, und versuchen Sie es erneut.",
|
||||
"cannot_reach": "Zugriff auf Xiaomi-API nicht möglich",
|
||||
"tzinfo_error": "Weitere Informationen: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Keine Geräte im Xiaomi Cloud-Konto"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Konfiguration gespeichert! [Erneut anpassen](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nEinige anpassbare Einstellungen müssen [neu geladen werden](/developer-tools/yaml) um wirksam zu werden.",
|
||||
"already_configured": "Gerät wurde bereits eingerichtet",
|
||||
"not_xiaomi_miio": "Kein Xiaomi miio/miot-Gerät."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Geräteinfo konfigurieren.",
|
||||
"data": {
|
||||
"host": "Lokale IP",
|
||||
"token": "Token",
|
||||
"scan_interval": "Status Update Interval (Sekunden)",
|
||||
"miot_cloud": "Aktiviere miot-Cloud (Konfiguriere Konto in YAML)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Konfiguriere Xiaomi-Cloud-Konto.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Konto ID / Email",
|
||||
"password": "Passwort",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Server-Standort der MiCloud",
|
||||
"conn_mode": "Verbindungsmodus für Gerät",
|
||||
"renew_devices": "Geräteerneuerung erzwingen",
|
||||
"disable_message": "Deaktiviere Mihome-Benachrichtigungssensor"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Cloud-Geräte filtern",
|
||||
"description": "Sie können Geräte ein- oder ausschließen, um nur die gewünschten Geräte zu integrieren. Wenn Sie im Ausschlussmodus keine Option in der Liste aktivieren, entspricht dies dem Einschließen aller Optionen.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Gerätemodell-Filter",
|
||||
"model_list": "Gerätemodell-Liste",
|
||||
"filter_home_id": "Zuhause filtern",
|
||||
"home_id_list": "Zuhause-Liste",
|
||||
"filter_ssid": "WiFi SSID filtern",
|
||||
"ssid_list": "WiFi SSID-Liste",
|
||||
"filter_bssid": "WiFi BSSID filtern",
|
||||
"bssid_list": "WiFi BSSID-Liste",
|
||||
"filter_did": "Gerät filtern",
|
||||
"did_list": "Geräteliste"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung zum Gerät fehlgeschlagen",
|
||||
"cannot_login": "Login in das Xiaomi Cloud-Konto fehlgeschlagen. Bitte überprüfe das Passwort. Wenn die Anmeldung nach mehreren Versuchen fehlschlägt, verwenden Sie bitte ein anderes Gerät im selben LAN wie der HA-Host, um sich auf der offiziellen Xiaomi-Website (mi.com) anzumelden, und versuchen Sie es erneut.",
|
||||
"cannot_reach": "Zugriff auf Xiaomi-API nicht möglich",
|
||||
"tzinfo_error": "See: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Keine Geräte im Xiaomi Cloud Konto"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Neue Integration hinzufügen](/_my_redirect/config_flow_start?domain=xiaomi_miot) um Anpassungen zu bearbeiten.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Komponenten-Version",
|
||||
"can_reach_server": "Reach Xiaomi API Server",
|
||||
"can_reach_spec": "Reach MIoT-Spec Server",
|
||||
"logged_accounts": "Eingeloggte Konten",
|
||||
"total_devices": "Gesamtzahl der MiHome-Geräte"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"config":{
|
||||
"flow_title":"Xiaomi Miot",
|
||||
"step":{
|
||||
"user":{
|
||||
"title":"Επιλέξτε Ενέργεια",
|
||||
"data":{
|
||||
"action":"Ενέργεια"
|
||||
}
|
||||
},
|
||||
"token":{
|
||||
"description":"Ρύθμιση παραμέτρων πληροφοριών συσκευής.",
|
||||
"data":{
|
||||
"host":"Τοπική IP",
|
||||
"token":"Σημείο",
|
||||
"name":"Όνομα συσκευής",
|
||||
"scan_interval":"Διάστημα κατάστασης ενημέρωσης (δευτερόλεπτα)"
|
||||
}
|
||||
},
|
||||
"cloud":{
|
||||
"description":"Διαμορφώστε τον λογαριασμό cloud της Xiaomi.\n{tip}",
|
||||
"data":{
|
||||
"username":"Αναγνωριστικό λογαριασμού / email Xiaomi",
|
||||
"password":"Κωδικός πρόσβασης",
|
||||
"captcha":"Captcha",
|
||||
"server_country":"Τοποθεσία διακομιστή του MiCloud",
|
||||
"conn_mode":"Λειτουργία σύνδεσης για τη συσκευή",
|
||||
"filter_models":"Φιλτράρισμα συσκευών μέσω μοντέλου/σπιτιού/WiFi (Για προχωρημένους)"
|
||||
}
|
||||
},
|
||||
"cloud_filter":{
|
||||
"title":"Φιλτράρισμα συσκευών cloud",
|
||||
"description":"Μπορείτε να συμπεριλάβετε ή να εξαιρέσετε συσκευές για να ενσωματώσετε μόνο τις συσκευές που θέλετε. Στη λειτουργία εξαίρεσης, εάν δεν επιλέξετε καμία επιλογή στη λίστα, είναι ισοδύναμη με τη συμπερίληψη όλων των επιλογών.\n{tip}",
|
||||
"data":{
|
||||
"filter_model":"Φιλτράρισμα μοντέλου συσκευής",
|
||||
"model_list":"Λίστα μοντέλων συσκευών",
|
||||
"filter_home_id":"Αρχική σελίδα φίλτρου",
|
||||
"home_id_list":"Αρχική Λίστα",
|
||||
"filter_ssid":"Φίλτρο WiFi SSID",
|
||||
"ssid_list":"Λίστα SSID WiFi",
|
||||
"filter_bssid":"Φιλτράρισμα WiFi BSSID",
|
||||
"bssid_list":"Λίστα BSSID WiFi",
|
||||
"filter_did":"Συσκευή φίλτρου",
|
||||
"did_list":"Λίστα συσκευών"
|
||||
}
|
||||
},
|
||||
"customizing":{
|
||||
"title":"Προσαρμογή οντοτήτων / συσκευών",
|
||||
"description":"{tip}",
|
||||
"data":{
|
||||
"domain":"Επιλέξτε τομέα οντότητας",
|
||||
"entity":"Επιλέξτε οντότητα",
|
||||
"model":"Επιλέξτε μοντέλο συσκευής",
|
||||
"model_specified":"Καθορισμένο μοντέλο συσκευής (Προαιρετικό)",
|
||||
"only_main_entity":"Μόνο κύριες (μητρικές) οντότητες",
|
||||
"reset_customizes":"Επαναφορά στις προεπιλεγμένες προσαρμογές",
|
||||
"bool2selects":"Προσαρμογή επιλογών"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error":{
|
||||
"cannot_connect":"Αποτυχία σύνδεσης της συσκευής",
|
||||
"cannot_login":"Αποτυχία σύνδεσης στον λογαριασμό cloud xiaomi, ελέγξτε τον κωδικό πρόσβασης. Εάν αποτύχετε πάντα να συνδεθείτε, συνδεθείτε για να mi.com στο ίδιο περιβάλλον με το τρέχον δίκτυο και προσπαθήστε ξανά.",
|
||||
"cannot_reach":"Δεν είναι δυνατή η πρόσβαση στο API της Xiaomi",
|
||||
"tzinfo_error":"Βλ.: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices":"Καμία συσκευή στον λογαριασμό cloud xiaomi"
|
||||
},
|
||||
"abort":{
|
||||
"config_saved":"Η διαμόρφωση αποθηκεύτηκε! [Προσαρμογή ξανά] (/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nΟρισμένες προσαρμόσιμες επιλογές απαιτούν [επαναφόρτωση ενοποίησης] (/εργαλεία προγραμματιστή/yaml) για να τεθούν σε ισχύ.",
|
||||
"already_configured":"Η συσκευή έχει ήδη ρυθμιστεί",
|
||||
"not_xiaomi_miio":"Δεν είναι συσκευή Xiaomi miio / miot."
|
||||
}
|
||||
},
|
||||
"options":{
|
||||
"step":{
|
||||
"user":{
|
||||
"title":"Xiaomi Miot",
|
||||
"description":"Ρύθμιση παραμέτρων πληροφοριών συσκευής.",
|
||||
"data":{
|
||||
"host":"Τοπική IP",
|
||||
"token":"Σημείο",
|
||||
"scan_interval":"Διάστημα κατάστασης ενημέρωσης (δευτερόλεπτα)",
|
||||
"miot_cloud":"Ενεργοποίηση του miot cloud (Λογαριασμός Config σε YAML)"
|
||||
}
|
||||
},
|
||||
"cloud":{
|
||||
"description":"Διαμορφώστε τον λογαριασμό cloud της Xiaomi.\n{tip}",
|
||||
"data":{
|
||||
"username":"Αναγνωριστικό λογαριασμού / email Xiaomi",
|
||||
"password":"Κωδικός πρόσβασης",
|
||||
"captcha":"Κάπτσα",
|
||||
"server_country":"Τοποθεσία διακομιστή του MiCloud",
|
||||
"conn_mode":"Λειτουργία σύνδεσης για τη συσκευή",
|
||||
"renew_devices":"Αναγκαστική ανανέωση συσκευών",
|
||||
"disable_message":"Απενεργοποιήστε τον αισθητήρα ειδοποιήσεων Mihome"
|
||||
}
|
||||
},
|
||||
"cloud_filter":{
|
||||
"title":"Φιλτράρισμα συσκευών cloud",
|
||||
"description":"Μπορείτε να συμπεριλάβετε ή να εξαιρέσετε συσκευές για να ενσωματώσετε μόνο τις συσκευές που θέλετε. Στη λειτουργία εξαίρεσης, εάν δεν επιλέξετε καμία επιλογή στη λίστα, είναι ισοδύναμη με τη συμπερίληψη όλων των επιλογών.\n{tip}",
|
||||
"data":{
|
||||
"filter_model":"Φιλτράρισμα μοντέλου συσκευής",
|
||||
"model_list":"Λίστα μοντέλων συσκευών",
|
||||
"filter_home_id":"Αρχική σελίδα φίλτρου",
|
||||
"home_id_list":"Αρχική Λίστα",
|
||||
"filter_ssid":"Φίλτρο WiFi SSID",
|
||||
"ssid_list":"Λίστα SSID WiFi",
|
||||
"filter_bssid":"Φιλτράρισμα WiFi BSSID",
|
||||
"bssid_list":"Λίστα BSSID WiFi",
|
||||
"filter_did":"Συσκευή φίλτρου",
|
||||
"did_list":"Λίστα συσκευών"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error":{
|
||||
"cannot_connect":"Αποτυχία σύνδεσης της συσκευής",
|
||||
"cannot_login":"Η σύνδεση στο λογαριασμό Mi απέτυχε, ελέγξτε τον κωδικό πρόσβασης του λογαριασμού. Εάν η σύνδεση αποτύχει μετά από αρκετές προσπάθειες, χρησιμοποιήστε μια άλλη συσκευή στο ίδιο LAN με τον κεντρικό υπολογιστή HA για να συνδεθείτε στον επίσημο ιστότοπο της Xiaomi (mi.com) και να προσπαθήσετε ξανά.",
|
||||
"cannot_reach":"Δεν είναι δυνατή η πρόσβαση στο API της Xiaomi",
|
||||
"tzinfo_error":"Βλ.: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices":"Καμία συσκευή στον λογαριασμό cloud xiaomi"
|
||||
},
|
||||
"abort":{
|
||||
"show_customizes":"[![Προσθήκη νέας ενσωμάτωσης] (https://my.home-assistant.io/badges/config_flow_start.svg)] (/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Προσθήκη νέας ενσωμάτωσης] (/_my_redirect/config_flow_start?domain=xiaomi_miot) για να τροποποιήσετε την προσαρμογή.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health":{
|
||||
"info":{
|
||||
"component_version":"Έκδοση στοιχείου",
|
||||
"can_reach_server":"Επικοινωνήστε με τον διακομιστή API της Xiaomi",
|
||||
"can_reach_spec":"Επικοινωνήστε με τον διακομιστή προδιαγραφών MIoT",
|
||||
"logged_accounts":"Συνδεδεμένοι λογαριασμοί",
|
||||
"total_devices":"Σύνολο συσκευών MiHome"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Select Action",
|
||||
"data": {
|
||||
"action": "Action"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Config device info.",
|
||||
"data": {
|
||||
"host": "Local IP",
|
||||
"token": "Token",
|
||||
"name": "Device name",
|
||||
"scan_interval": "Update state interval (seconds)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Config xiaomi cloud account.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Account ID / Email",
|
||||
"password": "Password",
|
||||
"captcha": "Captcha",
|
||||
"verify_ticket": "Two-factor verification code",
|
||||
"server_country": "Server location of MiCloud",
|
||||
"conn_mode": "Connection mode for device",
|
||||
"trans_options": "Translation property value description",
|
||||
"filter_models": "Filter devices via model/home/WiFi (Advanced)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filter cloud devices",
|
||||
"description": "You can include or exclude devices to integrate only the devices you want.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filter Device Model",
|
||||
"model_list": "Device Model List",
|
||||
"filter_home_id": "Filter Home",
|
||||
"home_id_list": "Home List",
|
||||
"filter_ssid": "Filter WiFi SSID",
|
||||
"ssid_list": "WiFi SSID List",
|
||||
"filter_bssid": "Filter WiFi BSSID",
|
||||
"bssid_list": "WiFi BSSID List",
|
||||
"filter_did": "Filter Device",
|
||||
"did_list": "Device List",
|
||||
"home_ids": "Filter Home (Leave blank to save)"
|
||||
},
|
||||
"data_description": {
|
||||
"filter_did": "In the exclude mode, if you do not check any option in the list, it is equivalent to including all devices.",
|
||||
"home_ids": "After checking this option and submitting, you will return to the current page,\nand the device list will refresh and filter out the devices of the checked homes."
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Customizing entities / devices",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Select entity domain",
|
||||
"entity": "Select entity",
|
||||
"model": "Select device model",
|
||||
"model_specified": "Specified device model (Optional)",
|
||||
"only_main_entity": "Only main (parent) entities",
|
||||
"yaml_mode": "YAML Mode",
|
||||
"yaml_customizes": "Customizes YAML Code",
|
||||
"reset_customizes": "Reset to default customizes",
|
||||
|
||||
"binary_sensor_attributes": "binary_sensor_attributes",
|
||||
"binary_sensor_properties": "binary_sensor_properties",
|
||||
"bind_sensor": "bind_sensor",
|
||||
"bind_xiaoai": "bind_xiaoai",
|
||||
"brightness_for_off": "brightness_for_off",
|
||||
"brightness_for_on": "brightness_for_on",
|
||||
"button_actions": "button_actions",
|
||||
"button_properties": "button_properties",
|
||||
"chunk_properties": "chunk_properties",
|
||||
"close_texts": "close_texts",
|
||||
"closed_position": "closed_position",
|
||||
"cloud_delay_update": "cloud_delay_update",
|
||||
"configuration_entities": "configuration_entities",
|
||||
"coord_type": "coord_type",
|
||||
"current_temp_property": "current_temp_property",
|
||||
"descriptions_for_on": "descriptions_for_on",
|
||||
"descriptions_for_off": "descriptions_for_off",
|
||||
"deviated_position": "deviated_position",
|
||||
"device_class": "device_class",
|
||||
"diagnostic_entities": "diagnostic_entities",
|
||||
"disable_location_name": "disable_location_name",
|
||||
"disable_preset_modes": "disable_preset_modes",
|
||||
"entity_category": "entity_category",
|
||||
"exclude_miot_properties": "exclude_miot_properties",
|
||||
"exclude_miot_services": "exclude_miot_services",
|
||||
"exclude_type": "exclude_type",
|
||||
"feeding_measure": "feeding_measure",
|
||||
"filter_home": "filter_home",
|
||||
"interval_seconds": "interval_seconds",
|
||||
"main_miot_services": "main_miot_services",
|
||||
"miot_did": "miot_did",
|
||||
"mitv_lan_host": "mitv_lan_host",
|
||||
"motion_stream_slice": "motion_stream_slice",
|
||||
"motion_timeout": "motion_timeout",
|
||||
"number_properties": "number_properties",
|
||||
"open_texts": "open_texts",
|
||||
"percentage_property": "percentage_property",
|
||||
"scanner_properties": "scanner_properties",
|
||||
"screenshot_compress": "screenshot_compress",
|
||||
"select_actions": "select_actions",
|
||||
"select_properties": "select_properties",
|
||||
"sensor_attributes": "sensor_attributes",
|
||||
"sensor_properties": "sensor_properties",
|
||||
"sources_via_apps": "sources_via_apps",
|
||||
"sources_via_keycodes": "sources_via_keycodes",
|
||||
"speed_property": "speed_property",
|
||||
"stat_power_cost_key": "stat_power_cost_key",
|
||||
"stat_power_cost_type": "stat_power_cost_type",
|
||||
"state_class": "state_class",
|
||||
"state_property": "state_property",
|
||||
"switch_properties": "switch_properties",
|
||||
"target_position_properties": "target_position_properties",
|
||||
"television_name": "television_name",
|
||||
"text_actions": "text_actions",
|
||||
"turn_on_hvac": "turn_on_hvac",
|
||||
"unit_of_measurement": "unit_of_measurement",
|
||||
"value_ratio": "value_ratio",
|
||||
"video_attribute": "video_attribute",
|
||||
"yeelight_smooth_off": "yeelight_smooth_off",
|
||||
"yeelight_smooth_on": "yeelight_smooth_on",
|
||||
|
||||
"bool2selects": "Customize options"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to the device",
|
||||
"cannot_login": "Failed to login to the Xiaomi account. Please check your account credentials. If login attempts fail multiple times, please try logging in again on the Xiaomi official website (mi.com) using the same network environment.",
|
||||
"cannot_reach": "Unable to access the Xiaomi API interface",
|
||||
"tzinfo_error": "There seems to be an issue with your Hass environment. Please refer to: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "No devices found under this Xiaomi account",
|
||||
"need_verify": "This login requires security verification. Please open the verification page to send the mobile/email verification code. After receiving the verification code, do not verify it on the webpage. Go back to HomeAssistant and enter it in the input box below."
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Configuration saved! [Customize again](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nSome custom options may require [reloading the integration](/developer-tools/yaml) to take effect.",
|
||||
"already_configured": "This device has already been configured.",
|
||||
"not_xiaomi_miio": "This device does not support the miio/miot protocol."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Config device info.",
|
||||
"data": {
|
||||
"host": "Device IP",
|
||||
"token": "Token",
|
||||
"scan_interval": "Status update interval (seconds)",
|
||||
"miot_cloud": "Enable miot cloud (Config account in YAML first)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Config xiaomi cloud account.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Account ID / Email",
|
||||
"password": "Password",
|
||||
"captcha": "Captcha",
|
||||
"verify_ticket": "Two-factor verification code",
|
||||
"server_country": "Server location of MiCloud",
|
||||
"conn_mode": "Connection mode for device",
|
||||
"renew_devices": "Force renew devices",
|
||||
"trans_options": "Translation property value description",
|
||||
"disable_message": "Disable Mihome notification sensor",
|
||||
"disable_scene_history": "Disable Mihome scene history sensor"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filter cloud devices",
|
||||
"description": "You can include or exclude devices to integrate only the devices you want.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filter Device Model",
|
||||
"model_list": "Device Model List",
|
||||
"filter_home_id": "Filter Home",
|
||||
"home_id_list": "Home List",
|
||||
"filter_ssid": "Filter WiFi SSID",
|
||||
"ssid_list": "WiFi SSID List",
|
||||
"filter_bssid": "Filter WiFi BSSID",
|
||||
"bssid_list": "WiFi BSSID List",
|
||||
"filter_did": "Filter Device",
|
||||
"did_list": "Device List",
|
||||
"home_ids": "Filter Home (Leave blank to save)"
|
||||
},
|
||||
"data_description": {
|
||||
"filter_did": "In the exclude mode, if you do not check any option in the list, it is equivalent to including all devices.",
|
||||
"home_ids": "After checking this option and submitting, you will return to the current page,\nand the device list will refresh and filter out the devices of the checked homes."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to the device",
|
||||
"cannot_login": "Failed to login to the Xiaomi account. Please check your account credentials. If login attempts fail multiple times, please try logging in again on the Xiaomi official website (mi.com) using the same network environment.",
|
||||
"cannot_reach": "Unable to access the Xiaomi API interface",
|
||||
"tzinfo_error": "There seems to be an issue with your Hass environment. Please refer to: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "No devices found under this Xiaomi account",
|
||||
"need_verify": "This login requires security verification. Please open the verification page to send the mobile/email verification code. After receiving the verification code, do not verify it on the webpage. Go back to HomeAssistant and enter it in the input box below."
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Add a new integration](/_my_redirect/config_flow_start?domain=xiaomi_miot) to modify customization.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Custom component version",
|
||||
"can_reach_server": "Reach Xiaomi API server",
|
||||
"can_reach_spec": "Reach MIoT-Spec Server",
|
||||
"logged_accounts": "Number of logged-in accounts",
|
||||
"total_devices": "Total number of MiHome devices"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"info": {"name": "Info"}
|
||||
},
|
||||
"sensor": {
|
||||
"clean_area": {"name": "Clean Area"},
|
||||
"clean_time": {"name": "Clean Time"},
|
||||
"power_cost_today": {"name": "Energy Today"},
|
||||
"power_cost_month": {"name": "Energy Month"},
|
||||
"power_cost_today_2": {"name": "Energy Today"},
|
||||
"power_cost_month_2": {"name": "Energy Month"},
|
||||
"props-clean_area": {"name": "Clean Area"},
|
||||
"props-clean_time": {"name": "Clean Time"},
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"password": "Password",
|
||||
"biological": "Biological",
|
||||
"key": "Key",
|
||||
"turntable": "Turntable",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "One-time Password",
|
||||
"two_step_verification": "Two-step Verification",
|
||||
"coercion": "Coercion",
|
||||
"homekit": "Homekit",
|
||||
"manual": "Manual",
|
||||
"automatic": "Automatic"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "Unlock from outside",
|
||||
"lock": "Lock",
|
||||
"anti_lock_on": "Enable anti-lock",
|
||||
"anti_lock_off": "Disable anti-lock",
|
||||
"inside_unlock": "Unlock from inside",
|
||||
"lock_inside": "Lock from inside",
|
||||
"child_lock_on": "Enable child lock",
|
||||
"child_lock_off": "Disable child lock",
|
||||
"lock_outside": "Lock from outside"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "Open",
|
||||
"close": "Close",
|
||||
"close_timeout": "Close timeout",
|
||||
"knock": "Knock",
|
||||
"breaking": "Break-in",
|
||||
"stuck": "Stuck"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Auto",
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"middle": "Medium",
|
||||
"high": "High",
|
||||
"quiet": "Quiet",
|
||||
"turbo": "Turbo",
|
||||
"level1": "Level 1",
|
||||
"level2": "Level 2",
|
||||
"level3": "Level 3",
|
||||
"level4": "Level 4",
|
||||
"level5": "Level 5",
|
||||
"level6": "Level 6",
|
||||
"level7": "Level 7"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"both": "Both"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"name": "Send command",
|
||||
"description": "Send miio command",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Method",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "Params",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_properties": {
|
||||
"name": "Get properties",
|
||||
"description": "Get miot properties",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"mapping": {
|
||||
"name": "Mapping",
|
||||
"description": ""
|
||||
},
|
||||
"update_entity": {
|
||||
"name": "Update entity",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_property": {
|
||||
"name": "Set property",
|
||||
"description": "Set miot property",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"field": {
|
||||
"name": "Field",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "Value",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_miot_property": {
|
||||
"name": "Set miot property",
|
||||
"description": "Set miot property by siid/piid",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"piid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"call_action": {
|
||||
"name": "Call action",
|
||||
"description": "Call miot action",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"aiid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"name": "Intelligent speaker",
|
||||
"description": "Intelligent speaker for XiaoAi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Xiaoai Entity ID"
|
||||
},
|
||||
"text": {
|
||||
"name": "Text",
|
||||
"description": ""
|
||||
},
|
||||
"execute": {
|
||||
"name": "Execute",
|
||||
"description": ""
|
||||
},
|
||||
"silent": {
|
||||
"name": "Silent",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"name": "XiaoAi Wakeup",
|
||||
"description": "Wakeup the XiaoAi speaker",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Xiaoai Entity ID"
|
||||
},
|
||||
"text": {
|
||||
"name": "Text",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_device_data": {
|
||||
"name": "Get device data",
|
||||
"description": "Get xiaomi device data from cloud",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"name": "Type",
|
||||
"description": ""
|
||||
},
|
||||
"key": {
|
||||
"name": "Key",
|
||||
"description": ""
|
||||
},
|
||||
"time_start": {
|
||||
"name": "Time start",
|
||||
"description": ""
|
||||
},
|
||||
"time_end": {
|
||||
"name": "Time end",
|
||||
"description": ""
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": ""
|
||||
},
|
||||
"group": {
|
||||
"name": "Group",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"renew_devices": {
|
||||
"name": "Renew devices",
|
||||
"description": "Renew cached xiaomi devices",
|
||||
"fields": {
|
||||
"username": {
|
||||
"name": "Username",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_token": {
|
||||
"name": "Get token",
|
||||
"description": "Get xiaomi device token",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Name",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_bindkey": {
|
||||
"name": "Get bindkey",
|
||||
"description": "Get bindkey for ble device",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"did": {
|
||||
"name": "DID",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"name": "Request xiaomi API",
|
||||
"description": "Request xiaomi API",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"api": {
|
||||
"name": "API",
|
||||
"description": ""
|
||||
},
|
||||
"data": {
|
||||
"name": "Data",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Method",
|
||||
"description": ""
|
||||
},
|
||||
"crypt": {
|
||||
"name": "Crypt",
|
||||
"description": ""
|
||||
},
|
||||
"sid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Seleccionar Acción",
|
||||
"data": {
|
||||
"action": "Acción"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Configurar información del dispositivo.",
|
||||
"data": {
|
||||
"host": "IP Local",
|
||||
"token": "Token",
|
||||
"name": "Nombre del dispositivo",
|
||||
"scan_interval": "Intervalo de actualización del estado (segundos)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Configurar cuenta en la nube de Xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID de cuenta / Email de Xiaomi",
|
||||
"password": "Contraseña",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Ubicación del servidor de MiCloud",
|
||||
"conn_mode": "Modo de conexión para el dispositivo",
|
||||
"filter_models": "Filtrar dispositivos por modelo/hogar/WiFi (Avanzado)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtrar dispositivos en la nube",
|
||||
"description": "Puedes incluir o excluir dispositivos para integrar solo los que desees. En el modo de exclusión, si no seleccionas ninguna opción en la lista, será equivalente a incluir todas las opciones.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtrar modelo de dispositivo",
|
||||
"model_list": "Lista de modelos de dispositivo",
|
||||
"filter_home_id": "Filtrar hogar",
|
||||
"home_id_list": "Lista de hogares",
|
||||
"filter_ssid": "Filtrar SSID de WiFi",
|
||||
"ssid_list": "Lista de SSID de WiFi",
|
||||
"filter_bssid": "Filtrar BSSID de WiFi",
|
||||
"bssid_list": "Lista de BSSID de WiFi",
|
||||
"filter_did": "Filtrar dispositivo",
|
||||
"did_list": "Lista de dispositivos"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Personalizar entidades / dispositivos",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Seleccionar dominio de la entidad",
|
||||
"entity": "Seleccionar entidad",
|
||||
"model": "Seleccionar modelo de dispositivo",
|
||||
"model_specified": "Especificar modelo de dispositivo (Opcional)",
|
||||
"only_main_entity": "Solo entidades principales (padre)",
|
||||
"reset_customizes": "Restaurar personalizaciones predeterminadas",
|
||||
"bool2selects": "Opciones personalizadas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar al dispositivo",
|
||||
"cannot_login": "No se pudo iniciar sesión en la cuenta de Xiaomi. Por favor, verifica tus credenciales de cuenta. Si los intentos de inicio de sesión fallan varias veces, intenta iniciar sesión nuevamente en el sitio web oficial de Xiaomi (mi.com) usando el mismo entorno de red.",
|
||||
"cannot_reach": "No se puede acceder a la interfaz de la API de Xiaomi",
|
||||
"tzinfo_error": "Parece que hay un problema con tu entorno de Hass. Por favor, consulta: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "No se encontraron dispositivos bajo esta cuenta de Xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "¡Configuración guardada! [Personalizar de nuevo](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nAlgunas opciones personalizadas pueden requerir [recargar la integración](/developer-tools/yaml) para que surtan efecto.",
|
||||
"already_configured": "Este dispositivo ya ha sido configurado.",
|
||||
"not_xiaomi_miio": "Este dispositivo no es compatible con el protocolo miio/miot."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Configurar información del dispositivo.",
|
||||
"data": {
|
||||
"host": "IP del dispositivo",
|
||||
"token": "Token",
|
||||
"scan_interval": "Intervalo de actualización de estado (segundos)",
|
||||
"miot_cloud": "Habilitar la nube de MIoT (Configurar la cuenta en YAML primero)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Configurar cuenta en la nube de Xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID de cuenta / Email de Xiaomi",
|
||||
"password": "Contraseña",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Ubicación del servidor de MiCloud",
|
||||
"conn_mode": "Modo de conexión para el dispositivo",
|
||||
"renew_devices": "Forzar renovación de dispositivos",
|
||||
"disable_message": "Desactivar el sensor de notificaciones de MiHome"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtrar dispositivos en la nube",
|
||||
"description": "Puedes incluir o excluir dispositivos para integrar solo los que desees. En el modo de exclusión, si no seleccionas ninguna opción en la lista, será equivalente a incluir todas las opciones.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtrar modelo de dispositivo",
|
||||
"model_list": "Lista de modelos de dispositivo",
|
||||
"filter_home_id": "Filtrar hogar",
|
||||
"home_id_list": "Lista de hogares",
|
||||
"filter_ssid": "Filtrar SSID de WiFi",
|
||||
"ssid_list": "Lista de SSID de WiFi",
|
||||
"filter_bssid": "Filtrar BSSID de WiFi",
|
||||
"bssid_list": "Lista de BSSID de WiFi",
|
||||
"filter_did": "Filtrar dispositivo",
|
||||
"did_list": "Lista de dispositivos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar al dispositivo",
|
||||
"cannot_login": "No se pudo iniciar sesión en la cuenta de Xiaomi. Por favor, verifica tus credenciales de cuenta. Si los intentos de inicio de sesión fallan varias veces, intenta iniciar sesión nuevamente en el sitio web oficial de Xiaomi (mi.com) usando el mismo entorno de red.",
|
||||
"cannot_reach": "No se puede acceder a la interfaz de la API de Xiaomi",
|
||||
"tzinfo_error": "Parece que hay un problema con tu entorno de Hass. Por favor, consulta: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "No se encontraron dispositivos bajo esta cuenta de Xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Agregar una nueva integración](/_my_redirect/config_flow_start?domain=xiaomi_miot) para modificar la personalización.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Versión del componente personalizado",
|
||||
"can_reach_server": "Acceso al servidor de la API de Xiaomi",
|
||||
"can_reach_spec": "Acceso al servidor MIoT-Spec",
|
||||
"logged_accounts": "Número de cuentas iniciadas de sesión",
|
||||
"total_devices": "Número total de dispositivos MiHome"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"password": "Contraseña",
|
||||
"biological": "Biológico",
|
||||
"key": "Llave",
|
||||
"turntable": "Plataforma giratoria",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "Contraseña de un solo uso",
|
||||
"two_step_verification": "Verificación de dos pasos",
|
||||
"coercion": "Coacción",
|
||||
"homekit": "Homekit",
|
||||
"manual": "Manual",
|
||||
"automatic": "Automático"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "Desbloquear desde el exterior",
|
||||
"lock": "Bloquear",
|
||||
"anti_lock_on": "Activar bloqueo anti-cierre",
|
||||
"anti_lock_off": "Desactivar bloqueo anti-cierre",
|
||||
"inside_unlock": "Desbloquear desde el interior",
|
||||
"lock_inside": "Bloquear desde el interior",
|
||||
"child_lock_on": "Activar bloqueo para niños",
|
||||
"child_lock_off": "Desactivar bloqueo para niños",
|
||||
"lock_outside": "Bloquear desde el exterior"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "Abierto",
|
||||
"close": "Cerrado",
|
||||
"close_timeout": "Tiempo de espera para cerrar",
|
||||
"knock": "Golpe",
|
||||
"breaking": "Intento de intrusión",
|
||||
"stuck": "Atascado"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Automático",
|
||||
"low": "Bajo",
|
||||
"medium": "Medio",
|
||||
"middle": "Medio",
|
||||
"high": "Alto",
|
||||
"quiet": "Silencioso",
|
||||
"turbo": "Turbo",
|
||||
"level1": "Nivel 1",
|
||||
"level2": "Nivel 2",
|
||||
"level3": "Nivel 3",
|
||||
"level4": "Nivel 4",
|
||||
"level5": "Nivel 5",
|
||||
"level6": "Nivel 6",
|
||||
"level7": "Nivel 7"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Desactivado",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"both": "Ambos"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Sélectionner une action",
|
||||
"data": {
|
||||
"action": "Action"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Configurer les informations de l'appareil.",
|
||||
"data": {
|
||||
"host": "IP locale",
|
||||
"token": "Jeton",
|
||||
"name": "Nom de l'appareil",
|
||||
"scan_interval": "Intervalle de mise à jour (en secondes)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Configurer le compte cloud Xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID / e-mail du compte Xiaomi",
|
||||
"password": "Mot de passe",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Emplacement du serveur MiCloud",
|
||||
"conn_mode": "Mode de connexion pour l'appareil",
|
||||
"filter_models": "Filtrer les appareils par modèle/maison/Wi-Fi (Avancé)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtrer les appareils du cloud",
|
||||
"description": "Vous pouvez inclure ou exclure des appareils pour n'intégrer que les appareils que vous souhaitez. En mode exclusion, si vous ne cochez aucune option dans la liste, cela équivaut à inclure toutes les options.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtrer par modèle d'appareil",
|
||||
"model_list": "Liste des modèles d'appareil",
|
||||
"filter_home_id": "Filtrer par maison",
|
||||
"home_id_list": "Liste des maisons",
|
||||
"filter_ssid": "Filtrer par SSID Wi-Fi",
|
||||
"ssid_list": "Liste des SSID Wi-Fi",
|
||||
"filter_bssid": "Filtrer par BSSID Wi-Fi",
|
||||
"bssid_list": "Liste des BSSID Wi-Fi",
|
||||
"filter_did": "Filtrer par appareil",
|
||||
"did_list": "Liste des appareils"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Personnaliser les entités / appareils",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Sélectionner le domaine de l'entité",
|
||||
"entity": "Sélectionner l'entité",
|
||||
"model": "Sélectionner le modèle de l'appareil",
|
||||
"model_specified": "Modèle d'appareil spécifié (facultatif)",
|
||||
"only_main_entity": "Uniquement les entités principales (parent)",
|
||||
"reset_customizes": "Réinitialiser les personnalisations par défaut",
|
||||
"bool2selects": "Options de personnalisation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossible de se connecter à l'appareil",
|
||||
"cannot_login": "Impossible de se connecter au compte Cloud Xiaomi, veuillez vérifier le mot de passe. Si vous n'arrivez pas à vous connecter, connectez-vous sur mi.com dans le même environnement que le réseau actuel et réessayez.",
|
||||
"cannot_reach": "Impossible d'accéder à l'API Xiaomi",
|
||||
"tzinfo_error": "Voir : https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Aucun appareil dans le compte Xiaomi Cloud"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Configuration enregistrée ! [Customize again](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nCertaines options personnalisables nécessitent un [rechargement de l'intégration ](/developer-tools/yaml) pour prendre effet.",
|
||||
"already_configured": "L'appareil est déjà configuré",
|
||||
"not_xiaomi_miio": "Pas un appareil Xiaomi miio/miot."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Configurer les informations de l'appareil.",
|
||||
"data": {
|
||||
"host": "Adresse IP locale",
|
||||
"token": "Jeton",
|
||||
"scan_interval": "Intervalle de mise à jour (secondes)",
|
||||
"miot_cloud": "Activer le cloud miot (Configurer le compte dans YAML)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Configurer le compte cloud Xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "Identifiant du compte Xiaomi / Email",
|
||||
"password": "Mot de passe",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Emplacement du serveur MiCloud",
|
||||
"conn_mode": "Mode de connexion pour l'appareil",
|
||||
"renew_devices": "Forcer la mise à jour des appareils",
|
||||
"disable_message": "Désactiver le capteur de notification Mihome"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtrer les appareils du cloud",
|
||||
"description": "Vous pouvez inclure ou exclure les appareils pour n'intégrer que les appareils que vous souhaitez. En mode exclusion, si vous ne cochez aucune option dans la liste, cela équivaut à inclure toutes les options.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtrer le modèle d'appareil",
|
||||
"model_list": "Liste des modèles d'appareils",
|
||||
"filter_home_id": "Filtrer le domicile",
|
||||
"home_id_list": "Liste des domiciles",
|
||||
"filter_ssid": "Filtrer le SSID WiFi",
|
||||
"ssid_list": "Liste des SSID WiFi",
|
||||
"filter_bssid": "Filtrer le BSSID WiFi",
|
||||
"bssid_list": "Liste des BSSID WiFi",
|
||||
"filter_did": "Filtrer l'appareil",
|
||||
"did_list": "Liste des appareils"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossible de se connecter à l'appareil",
|
||||
"cannot_login": "Échec de connexion au compte Xiaomi, veuillez vérifier le mot de passe. Si la connexion échoue plusieurs fois, connectez-vous au site web de mi.com sur un autre appareil du même réseau local que celui de HA et essayez à nouveau.",
|
||||
"cannot_reach": "Impossible d'accéder à l'API Xiaomi",
|
||||
"tzinfo_error": "Voir: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Aucun appareil dans le compte cloud Xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Ajouter une nouvelle intégration](/_my_redirect/config_flow_start?domain=xiaomi_miot) pour modifier la personnalisation.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Version de composant",
|
||||
"can_reach_server": "Connexion serveur Xiaomi API",
|
||||
"can_reach_spec": "Connexion serveur MIoT-Spec",
|
||||
"logged_accounts": "Comptes connectés",
|
||||
"total_devices": "Total appareils MiHome"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Válasszon műveletet",
|
||||
"data": {
|
||||
"action": "Művelet"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Konfigurálja az eszközinformációkat.",
|
||||
"data": {
|
||||
"host": "Helyi IP",
|
||||
"token": "Token",
|
||||
"name": "Eszköz neve",
|
||||
"scan_interval": "Állapot frissítési időköz (másodperc)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Konfigurálja a Xiaomi felhőfiókot.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi fiók azonosító / E-mail",
|
||||
"password": "Jelszó",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "MiCloud szerver régió",
|
||||
"conn_mode": "Kapcsolat módja az eszközhöz",
|
||||
"filter_models": "Eszközök szűrése modell/otthon/WiFi alapján (speciális)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Felhő eszközök szűrése",
|
||||
"description": "Beleveheti vagy kizárhatja az eszközöket, hogy csak azokat az eszközöket integrálja, amelyekiet szeretné. A kizárás módban, ha nem jelöl be semmilyen lehetőséget a listán, az egyenértékű az összes lehetőség beillesztésével. \n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Eszközmodell szűrése",
|
||||
"model_list": "Eszközmodell lista",
|
||||
"filter_home_id": "Otthon szűrése",
|
||||
"home_id_list": "Otthon lista",
|
||||
"filter_ssid": "WiFi SSID szűrése",
|
||||
"ssid_list": "WiFi SSID lista",
|
||||
"filter_bssid": "WiFi BSSID szűrése",
|
||||
"bssid_list": "WiFi BSSID lista",
|
||||
"filter_did": "Eszköz szűrése",
|
||||
"did_list": "Eszköz lista"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Entitások / eszközök testreszabása",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Válasszon entitás tartományt",
|
||||
"entity": "Válassza ki az entitást",
|
||||
"model": "Válassza ki az eszközmodellt",
|
||||
"model_specified": "Specifikus eszközmodell (opcionális)",
|
||||
"only_main_entity": "Csak fő (szülő) entitások",
|
||||
"reset_customizes": "Alapértelmezett testreszabások visszaállítása",
|
||||
"bool2selects": "Testreszabási lehetőségek"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nem sikerült kapcsolódni az eszközhöz",
|
||||
"cannot_login": "Nem sikerült bejelentkezni a Xiaomi fiókba. Kérjük, ellenőrizze a fiók adatait. Ha a bejelentkezési kísérletek többször is sikertelenek, próbálkozzon újra a Xiaomi hivatalos weboldalán (mi.com) ugyanazon hálózati környezetben.",
|
||||
"cannot_reach": "Nem sikerült elérni a Xiaomi API felületet",
|
||||
"tzinfo_error": "Probléma van a Hass környezetével. Kérjük, olvassa el a következő leírást: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Nincsenek eszközök ezen Xiaomi fiók alatt"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Konfiguráció mentve! [Testreszabás újra](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nNéhány testreszabási lehetőség érvénybe léptetéséhez [újra kell tölteni az integrációt](/developer-tools/yaml).",
|
||||
"already_configured": "Ez az eszköz már konfigurálva van.",
|
||||
"not_xiaomi_miio": "Ez az eszköz nem támogatja a miio/miot protokolt."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Konfigurálja az eszközinformációkat.",
|
||||
"data": {
|
||||
"host": "Eszköz IP",
|
||||
"token": "Token",
|
||||
"scan_interval": "Állapot frissítési időköz (másodperc)",
|
||||
"miot_cloud": "Miot felhő engedélyezése (Először konfigurálja a fiókot a YAML-ben)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Konfigurálja a Xiaomi felhőfiókot.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi fiók azonosító / E-mail",
|
||||
"password": "Jelszó",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "MiCloud szerver régió",
|
||||
"conn_mode": "Kapcsolat módja az eszközhöz",
|
||||
"renew_devices": "Eszközök kényszerített frissítése",
|
||||
"disable_message": "Mihome értesítés szenzor letiltása"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Felhő eszközök szűrése",
|
||||
"description": "Beleveheti vagy kizárhatja az eszközöket, hogy csak azokat az eszközöket integrálja, amelyekiet szeretné. A kizárás módban, ha nem jelöl be semmilyen lehetőséget a listán, az egyenértékű az összes lehetőség beillesztésével. \n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Eszközmodell szűrése",
|
||||
"model_list": "Eszközmodell lista",
|
||||
"filter_home_id": "Otthon szűrése",
|
||||
"home_id_list": "Otthon lista",
|
||||
"filter_ssid": "WiFi SSID szűrése",
|
||||
"ssid_list": "WiFi SSID lista",
|
||||
"filter_bssid": "WiFi BSSID szűrése",
|
||||
"bssid_list": "WiFi BSSID lista",
|
||||
"filter_did": "Eszköz szűrése",
|
||||
"did_list": "Eszköz lista"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nem sikerült kapcsolódni az eszközhöz",
|
||||
"cannot_login": "Nem sikerült bejelentkezni a Xiaomi fiókba. Kérjük, ellenőrizze a fiók adatait. Ha a bejelentkezési kísérletek többször is sikertelenek, próbálkozzon újra a Xiaomi hivatalos weboldalán (mi.com) ugyanazon hálózati környezetben.",
|
||||
"cannot_reach": "Nem sikerült elérni a Xiaomi API felületet",
|
||||
"tzinfo_error": "Probléma van a Hass környezetével. Kérjük, olvassa el a következő leírást: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Nincsenek eszközök ezen Xiaomi fiók alatt"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Új integráció hozzáadása](/_my_redirect/config_flow_start?domain=xiaomi_miot) a testreszabás módosításához.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Egyedi komponens verzió",
|
||||
"can_reach_server": "Elérheti a Xiaomi API szerverét",
|
||||
"can_reach_spec": "Elérheti a MIoT-Spec szerverét",
|
||||
"logged_accounts": "Bejelentkezett fiókok száma",
|
||||
"total_devices": "Összes MiHome eszköz száma"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"password": "Jelszó",
|
||||
"biological": "Biometrikus",
|
||||
"key": "Kulcs",
|
||||
"turntable": "Forgómechanizmus",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "Egyszer használatos jelszó",
|
||||
"two_step_verification": "Kétlépcsős azonosítás",
|
||||
"coercion": "Kényszerítés",
|
||||
"homekit": "Homekit",
|
||||
"manual": "Kézi",
|
||||
"automatic": "Automatikus"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "Kívülről kinyitás",
|
||||
"lock": "Zárás",
|
||||
"anti_lock_on": "Anti-lock engedélyezése",
|
||||
"anti_lock_off": "Anti-lock letiltása",
|
||||
"inside_unlock": "Belső oldalról kinyitás",
|
||||
"lock_inside": "Belső oldalról zárás",
|
||||
"child_lock_on": "Gyermekzár engedélyezése",
|
||||
"child_lock_off": "Gyermekzár letiltása",
|
||||
"lock_outside": "Kívülről zárás"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "Nyitott",
|
||||
"close": "Zárt",
|
||||
"close_timeout": "Zárás időkorlátja",
|
||||
"knock": "Kopogás",
|
||||
"breaking": "Betörés",
|
||||
"stuck": "Beragadt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Automatikus",
|
||||
"low": "Alacsony",
|
||||
"medium": "Közepes",
|
||||
"middle": "Közepes",
|
||||
"high": "Magas",
|
||||
"quiet": "Csendes",
|
||||
"turbo": "Turbo",
|
||||
"level1": "Szint 1",
|
||||
"level2": "Szint 2",
|
||||
"level3": "Szint 3",
|
||||
"level4": "Szint 4",
|
||||
"level5": "Szint 5",
|
||||
"level6": "Szint 6",
|
||||
"level7": "Szint 7"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Kikapcsolva",
|
||||
"vertical": "Függőleges",
|
||||
"horizontal": "Vízszintes",
|
||||
"both": "Mindkettő"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"name": "Parancs küldése",
|
||||
"description": "Miio parancs küldése"
|
||||
},
|
||||
"get_properties": {
|
||||
"name": "Tulajdonságok lekérése",
|
||||
"description": "Miot tulajdonságok lekérése"
|
||||
},
|
||||
"set_property": {
|
||||
"name": "Tulajdonság beállítása",
|
||||
"description": "Miot tulajdonság beállítása"
|
||||
},
|
||||
"set_miot_property": {
|
||||
"name": "Miot tulajdonság beállítása",
|
||||
"description": "Miot tulajdonság beállítása siid/piid alapján"
|
||||
},
|
||||
"call_action": {
|
||||
"name": "Művelet végrehajtása",
|
||||
"description": "Miot művelet végrehajtása"
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"name": "Intelligens hangszóró",
|
||||
"description": "Intelligens hangszóró XiaoAi számára"
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"name": "XiaoAi felébresztése",
|
||||
"description": "XiaoAi hangszóró felébresztése"
|
||||
},
|
||||
"get_device_data": {
|
||||
"name": "Eszközadatok lekérése",
|
||||
"description": "Xiaomi eszközadatok lekérése a felhőből"
|
||||
},
|
||||
"renew_devices": {
|
||||
"name": "Eszközök megújítása",
|
||||
"description": "Gyorsítótárban lévő Xiaomi eszközök megújítása"
|
||||
},
|
||||
"get_token": {
|
||||
"name": "Token lekérése",
|
||||
"description": "Xiaomi eszköz token lekérése"
|
||||
},
|
||||
"get_bindkey": {
|
||||
"name": "Kötési kulcs lekérése",
|
||||
"description": "Kötési kulcs lekérése BLE eszközhöz"
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"name": "Xiaomi API lekérése",
|
||||
"description": "Xiaomi API lekérése"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Seleziona Azione",
|
||||
"data": {
|
||||
"action": "Azione"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Configurazione informazioni dispositivo.",
|
||||
"data": {
|
||||
"host": "IP Locale",
|
||||
"token": "Token",
|
||||
"name": "Nome Dispositivo",
|
||||
"scan_interval": "Intervallo (secondi)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Configurazione account xiaomi cloud.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Account ID / Email",
|
||||
"password": "Password",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Posizione server MiCloud",
|
||||
"conn_mode": "Modalità di connessione del dispostivo",
|
||||
"filter_models": "Filtra dispositivi per modello/famiglia/WiFi (Avanzato)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtro dispositivi cloud",
|
||||
"description": "Puoi includere o escludere dispositivi per integrare solo i dipositivi preferiti. In modalità esclusione, se non si seleziona nessuna opzione, vengono considerate tutte le opzioni come incluse.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtro Modello Dispositivo",
|
||||
"model_list": "Lista Modelli Dispositivi",
|
||||
"filter_home_id": "Filtra per casa",
|
||||
"home_id_list": "Lista case",
|
||||
"filter_ssid": "Filtro WiFi SSID",
|
||||
"ssid_list": "WiFi SSID List",
|
||||
"filter_bssid": "Filtro WiFi BSSID",
|
||||
"bssid_list": "WiFi BSSID List",
|
||||
"filter_did": "Filtro Dispositivo",
|
||||
"did_list": "Lista Dispositivi"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Presonalizzazione entità / dispositivi",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Seleziona dominio entità",
|
||||
"entity": "Seleziona entità",
|
||||
"model": "Seleziona modello dispositivo",
|
||||
"model_specified": "Modello dispositivo specificato (opzionale)",
|
||||
"only_main_entity": "Solo entità principali (parent)",
|
||||
"reset_customizes": "Ripristina le opzioni predefinite",
|
||||
"bool2selects": "Presonalizza opzioni"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Connessione al dispositivo fallita",
|
||||
"cannot_login": "Login all'account xiaomi cloud fallito, per favore controlla che la password sia corretta. Seil login continua a fallrie, effettua il login a mi.com inello stesso ambiente del corrente network e riprova.",
|
||||
"cannot_reach": " Impossibile accedere all'API Xiaomi",
|
||||
"tzinfo_error": "Guardare: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Nessun dispositivo nell'account xiaomi cloud"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Configurazione salvata! [Personalizza di nuovo](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nAlcune opzioni di personalizzazione richiedono [reload integration](/developer-tools/yaml) per avere effetto.",
|
||||
"already_configured": "Il dispositivo è già configurato",
|
||||
"not_xiaomi_miio": "Non è un dispositivo Xiaomi miio/miot."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Configurazione informazioni dispositivo.",
|
||||
"data": {
|
||||
"host": "IP Locale",
|
||||
"token": "Token",
|
||||
"scan_interval": "Intervallo aggiornamento stato (secondi)",
|
||||
"miot_cloud": "Abilita miot cloud (Configura account in YAML)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Configurazione xiaomi cloud account.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Account ID / Email",
|
||||
"password": "Password",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Posizione server di MiCloud",
|
||||
"conn_mode": "Modalità di connessione dispositivo",
|
||||
"renew_devices": "Forza rinnovo dispositivi",
|
||||
"disable_message": "Disattiva il sensore di notifica di Mihome",
|
||||
"disable_scene_history": "Disabilita sensore cronologia scene Mihome"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtro dispositivi cloud",
|
||||
"description": "Puoi includere o escludere dispositivi per integrare solo i dipositivi preferiti. In modalità esclusione, se non si seleziona nessuna opzione, vengono considerate tutte le opzioni come incluse.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtro Modello Dispositivo",
|
||||
"model_list": "Lista Modelli Dispositivi",
|
||||
"filter_home_id": "Filtra per casa",
|
||||
"home_id_list": "Lista case",
|
||||
"filter_ssid": "Filtro WiFi SSID",
|
||||
"ssid_list": "WiFi SSID List",
|
||||
"filter_bssid": "Filtro WiFi BSSID",
|
||||
"bssid_list": "WiFi BSSID List",
|
||||
"filter_did": "Filtro Dispositivo",
|
||||
"did_list": "Lista Dispositivi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Connessione al dispositivo fallita",
|
||||
"cannot_login": "Login all'account xiaomi cloud fallito, per favore controlla che la password sia corretta. Seil login continua a fallrie, effettua il login a mi.com inello stesso ambiente del corrente network e riprova.",
|
||||
"cannot_reach": " Impossibile accedere all'API Xiaomi",
|
||||
"tzinfo_error": "Guardare: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Nessun dispositivo nell'account xiaomi cloud"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Aggiungi nuova integrazione](/_my_redirect/config_flow_start?domain=xiaomi_miot) per modificare la personalizzazione.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Versione Componente",
|
||||
"can_reach_server": "Raggiunge Server Xiaomi API",
|
||||
"can_reach_spec": "Raggiunge Server MIoT-Spec",
|
||||
"logged_accounts": "Accaount Loggati",
|
||||
"total_devices": "Dispositivi MiHome Totali"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"password": "Password",
|
||||
"biological": "Biologica",
|
||||
"key": "Chiave",
|
||||
"turntable": "Giradischi",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "Password monouso",
|
||||
"two_step_verification": "Verifica in due passaggi",
|
||||
"coercion": "Coercizione",
|
||||
"homekit": "Homekit",
|
||||
"manual": "Manuale",
|
||||
"automatic": "Automatica"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "Sblocco dall'esterno",
|
||||
"lock": "Blocca",
|
||||
"anti_lock_on": "Attiva anti-lock",
|
||||
"anti_lock_off": "Disattiva anti-lock",
|
||||
"inside_unlock": "Sblocco dall'interno",
|
||||
"lock_inside": "Blocco dall'interno",
|
||||
"child_lock_on": "Attiva blocco bambini",
|
||||
"child_lock_off": "Disattiva blocco bambini",
|
||||
"lock_outside": "Blocco dall'esterno"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "Aperta",
|
||||
"close": "Chiusa",
|
||||
"close_timeout": "Chiusura timeout",
|
||||
"knock": "Bussata",
|
||||
"breaking": "Tentativo di effrazione",
|
||||
"stuck": "Bloccata"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Auto",
|
||||
"low": "Bassa",
|
||||
"medium": "Media",
|
||||
"middle": "Media",
|
||||
"high": "Alta",
|
||||
"quiet": "Silenziosa",
|
||||
"turbo": "Turbo",
|
||||
"level1": "Livello 1",
|
||||
"level2": "Livello 2",
|
||||
"level3": "Livello 3",
|
||||
"level4": "Livello 4",
|
||||
"level5": "Livello 5",
|
||||
"level6": "Livello 6",
|
||||
"level7": "Livello 7"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Spento",
|
||||
"vertical": "Verticale",
|
||||
"horizontal": "Orizontale",
|
||||
"both": "Entrambi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"name": "Invia comando",
|
||||
"description": "Invia comando miio",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Metodo",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "Parametro",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_properties": {
|
||||
"name": "Ottieni proprietà",
|
||||
"description": "Ottieni proprietà miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": ""
|
||||
},
|
||||
"mapping": {
|
||||
"name": "Mappatura",
|
||||
"description": ""
|
||||
},
|
||||
"update_entity": {
|
||||
"name": "Update entity",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_property": {
|
||||
"name": "Imposta proprietà",
|
||||
"description": "Imposta proprietà miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": ""
|
||||
},
|
||||
"field": {
|
||||
"name": "Campo",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "Valore",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_miot_property": {
|
||||
"name": "Imposta proprietà miot",
|
||||
"description": "Imposta proprietà miot tramite siid/piid",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"piid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"call_action": {
|
||||
"name": "Chiamata azione",
|
||||
"description": "Chiama azione miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"aiid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"name": "Altoparlante intelligente",
|
||||
"description": "Altoparlante intelligente per XiaoAi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": "ID entità Xiaoai"
|
||||
},
|
||||
"text": {
|
||||
"name": "Text",
|
||||
"description": ""
|
||||
},
|
||||
"execute": {
|
||||
"name": "Execute",
|
||||
"description": ""
|
||||
},
|
||||
"silent": {
|
||||
"name": "Silent",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"name": "Sveglia XiaoAi",
|
||||
"description": "Sveglia l'altoparlante XiaoAi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": "ID entità Xiaoai"
|
||||
},
|
||||
"text": {
|
||||
"name": "Testo",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_device_data": {
|
||||
"name": "Ottieni dati del dispositivo",
|
||||
"description": "Ottieni dati del dispositivo Xiaomi dal cloud",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"name": "Tipo",
|
||||
"description": ""
|
||||
},
|
||||
"key": {
|
||||
"name": "Chiave",
|
||||
"description": ""
|
||||
},
|
||||
"time_start": {
|
||||
"name": "Tempo di avvio",
|
||||
"description": ""
|
||||
},
|
||||
"time_end": {
|
||||
"name": "Tempo di fine",
|
||||
"description": ""
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limite",
|
||||
"description": ""
|
||||
},
|
||||
"group": {
|
||||
"name": "Groupo",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"renew_devices": {
|
||||
"name": "Rinnova dispositivi",
|
||||
"description": "Rinnova dispositivi Xiaomi in cache",
|
||||
"fields": {
|
||||
"username": {
|
||||
"name": "Username",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_token": {
|
||||
"name": "Ottieni token",
|
||||
"description": "Ottieni token del dispositivo Xiaomi",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Nome",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_bindkey": {
|
||||
"name": "Ottieni bindkey",
|
||||
"description": "Ottieni bindkey per dispositivo BLE",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": ""
|
||||
},
|
||||
"did": {
|
||||
"name": "DID",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"name": "Richiedi API Xiaomi",
|
||||
"description": "Richiedi API Xiaomi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID entità",
|
||||
"description": ""
|
||||
},
|
||||
"api": {
|
||||
"name": "API",
|
||||
"description": ""
|
||||
},
|
||||
"data": {
|
||||
"name": "Dati",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Metodo",
|
||||
"description": ""
|
||||
},
|
||||
"crypt": {
|
||||
"name": "Cifratura",
|
||||
"description": ""
|
||||
},
|
||||
"sid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Wybierz akcję",
|
||||
"data": {
|
||||
"action": "Akcja"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Skonfiguruj dane urządzenia.",
|
||||
"data": {
|
||||
"host": "Lokalny adres IP",
|
||||
"token": "Token",
|
||||
"name": "Nazwa urządzenia",
|
||||
"scan_interval": "Interwał aktualizacji stanu (sekundy)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Skonfiguruj konto w chmurze Xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "Identyfikator konta Xiaomi / Email",
|
||||
"password": "Hasło",
|
||||
"captcha": "Kod Captcha",
|
||||
"server_country": "Lokalizacja serwera MiCloud",
|
||||
"conn_mode": "Tryb połączenia dla urządzenia",
|
||||
"filter_models": "Filtruj urządzenia według modelu/domu/WiFi (zaawansowane)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtruj urządzenia w chmurze",
|
||||
"description": "Możesz uwzględnić lub wykluczyć urządzenia, aby zintegrować tylko wybrane urządzenia. W trybie wykluczania, jeśli nie zaznaczysz żadnej opcji na liście, jest to równoznaczne z włączeniem wszystkich opcji.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtruj model urządzenia",
|
||||
"model_list": "Lista modeli urządzeń",
|
||||
"filter_home_id": "Filtruj domy",
|
||||
"home_id_list": "Lista domów",
|
||||
"filter_ssid": "Filtruj SSID Wi-Fi",
|
||||
"ssid_list": "Lista identyfikatorów SSID Wi-Fi",
|
||||
"filter_bssid": "Filtruj BSSID Wi-Fi",
|
||||
"bssid_list": "Lista identyfikatorów BSSID Wi-Fi",
|
||||
"filter_did": "Filtruj urządzenia",
|
||||
"did_list": "Lista urządzeń"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Dostosowywanie encji / urządzeń",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Wybierz domenę encji",
|
||||
"entity": "Wybierz encję",
|
||||
"model": "Wybierz model urządzenia",
|
||||
"model_specified": "Określony model urządzenia (opcjonalnie)",
|
||||
"only_main_entity": "Wyłącznie encje główne (nadrzędne).",
|
||||
"reset_customizes": "Zresetuj do ustawień domyślnych",
|
||||
"bool2selects": "Dostosuj opcje"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nie udało się podłączyć urządzenia",
|
||||
"cannot_login": "Nie udało się zalogować do konta w chmurze Xiaomi, sprawdź hasło. Jeśli nigdy nie uda Ci się zalogować, zaloguj się do mi.com w tym samym środowisku co bieżąca sieć i spróbuj ponownie.",
|
||||
"cannot_reach": "Nie można uzyskać dostępu do interfejsu API Xiaomi",
|
||||
"tzinfo_error": "Zobacz: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Brak urządzeń na koncie w chmurze Xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Konfiguracja zapisana! [Customize again](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nSome customizable options require [reload integration](/developer-tools/yaml) to take effect.",
|
||||
"already_configured": "Urządzenie jest już skonfigurowane",
|
||||
"not_xiaomi_miio": "To nie jest urządzenie Xiaomi miio/miot."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Skonfiguruj dane urządzenia.",
|
||||
"data": {
|
||||
"host": "Lokalny adres IP",
|
||||
"token": "Token",
|
||||
"scan_interval": "Interwał aktualizacji stanu (sekundy)",
|
||||
"miot_cloud": "Włącz chmurę miot (Konfiguruj konto w YAML)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Skonfiguruj konto w chmurze Xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "Identyfikator konta Xiaomi / Email",
|
||||
"password": "Hasło",
|
||||
"captcha": "Kod Captcha",
|
||||
"server_country": "Lokalizacja serwera MiCloud",
|
||||
"conn_mode": "Tryb połączenia dla urządzenia",
|
||||
"renew_devices": "Wymuś odnowienie urządzeń",
|
||||
"disable_message": "Wyłącz sensor powiadomień Mihome"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtruj urządzenia w chmurze",
|
||||
"description": "Możesz uwzględnić lub wykluczyć urządzenia, aby zintegrować tylko wybrane urządzenia. W trybie wykluczania, jeśli nie zaznaczysz żadnej opcji na liście, jest to równoznaczne z włączeniem wszystkich opcji.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtruj model urządzenia",
|
||||
"model_list": "Lista modeli urządzeń",
|
||||
"filter_home_id": "Filtruj domy",
|
||||
"home_id_list": "Lista domów",
|
||||
"filter_ssid": "Filtruj SSID Wi-Fi",
|
||||
"ssid_list": "Lista identyfikatorów SSID Wi-Fi",
|
||||
"filter_bssid": "Filtruj BSSID Wi-Fi",
|
||||
"bssid_list": "Lista identyfikatorów BSSID Wi-Fi",
|
||||
"filter_did": "Filtruj urządzenia",
|
||||
"did_list": "Lista urządzeń"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nie udało się podłączyć urządzenia",
|
||||
"cannot_login": "Jeśli logowanie nie powiedzie się po kilku próbach, użyj innego urządzenia w tej samej sieci LAN, co host HA, aby zalogować się na oficjalnej stronie Xiaomi (mi.com) i spróbuj ponownie.",
|
||||
"cannot_reach": "Nie można uzyskać dostępu do interfejsu API Xiaomi",
|
||||
"tzinfo_error": "Zobacz: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Brak urządzeń na koncie w chmurze Xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Add a new integration](/_my_redirect/config_flow_start?domain=xiaomi_miot) to modify customization.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Wersja komponentu",
|
||||
"can_reach_server": "Serwer API Xiaomi dostępny",
|
||||
"can_reach_spec": "Serwer MIoT-Spec dostępny",
|
||||
"logged_accounts": "Zalogowane konta",
|
||||
"total_devices": "Całkowita liczba urządzeń MiHome"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Selecione a forma de integração",
|
||||
"data": {
|
||||
"action": "Opções"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Config infor do dispositivo.",
|
||||
"data": {
|
||||
"host": "IP Local",
|
||||
"token": "Token",
|
||||
"name": "Nome do dispositivo",
|
||||
"scan_interval": "Intervalo de update do estado (segundos)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Configurar pela conta xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID de conta Xiaomi / Email",
|
||||
"password": "Senha",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Região da MiCloud",
|
||||
"conn_mode": "Modo de conexão com o dispositivo",
|
||||
"filter_models": "Filtro de dispositivo via model/home/WiFi (Advanced)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtro de dispositivos da nuvem",
|
||||
"description": "Você pode incluir ou excluir dispositivos para a integrar só o que quiser. No modo de exclusão, se você não marcar nenhuma opção da lista, equivale a incluir todas as opções.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtro de Dispositivos",
|
||||
"model_list": "Lista de Dispositivos",
|
||||
"filter_home_id": "Filtro de Casa",
|
||||
"home_id_list": "Lista de Casa",
|
||||
"filter_ssid": "Filtro por WiFi SSID",
|
||||
"ssid_list": "Lista de WiFi SSID",
|
||||
"filter_bssid": "Filtro WiFi BSSID",
|
||||
"bssid_list": "Lista de WiFi BSSID",
|
||||
"filter_did": "Filtrar dispositivos",
|
||||
"did_list": "Lista de dispositivos"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Customizando entidades / dispositivos",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Selecione o domomínio da entidade",
|
||||
"entity": "Selecione a entidade",
|
||||
"model": "Selecione o modelo do dispositivo",
|
||||
"model_specified": "Modelo do dispositivo especificaco (Opcional)",
|
||||
"only_main_entity": "Apenas entidades principais (pais)",
|
||||
"reset_customizes": "Reiniciar para customização padrão",
|
||||
"bool2selects": "Opções de customização"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Falha em conectar o dispositivo",
|
||||
"cannot_login": "Falha no login com a conta xiaomi, verifique a senha. Se está tendo falhas ao fazer o login, faça login em mi.com na mesma rede atual e tente novamente.",
|
||||
"cannot_reach": "Incapaz de accessar a API Xiaomi",
|
||||
"tzinfo_error": "Aparentemente há um problema no seu ambiente Hass. Acesse: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Sem dispositivos na conta xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Configuração guardada! [Customize novamente](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nAlgumas opções de customização podem requerir [recarregar a integração](/developer-tools/yaml) para ter efeito.",
|
||||
"already_configured": "O dispositivo já está configurado",
|
||||
"not_xiaomi_miio": "Não é um dispositivo Xiaomi miio/miot."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Configurar info do dispositivo.",
|
||||
"data": {
|
||||
"host": "IP Local",
|
||||
"token": "Token",
|
||||
"scan_interval": "Intervalo de update do estado (segundos)",
|
||||
"miot_cloud": "Habilitar miot cloud (Configurar conta no YAML)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Config conta xiaomi.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID de conta Xiaomi / Email",
|
||||
"password": "Senha",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Região da MiCloud",
|
||||
"conn_mode": "Modo de conexão com o dispositivo",
|
||||
"renew_devices": "Forçar renovar dispositivos",
|
||||
"disable_message": "Desabilitar o sensor de notificação Mihome",
|
||||
"disable_scene_history": "Desabilitar o histórico do sensor de cena"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Filtro de dispositivos da nuvem",
|
||||
"description": "Você pode incluir ou excluir dispositivos para a integrar só o que quiser. No modo de exclusão, se você não marcar nenhuma opção da lista, equivale a incluir todas as opções.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Filtro de Dispositivos",
|
||||
"model_list": "Lista de Dispositivos",
|
||||
"filter_home_id": "Filtrar Casa",
|
||||
"home_id_list": "Lista Casa",
|
||||
"filter_ssid": "Filtro por WiFi SSID",
|
||||
"ssid_list": "Lista de WiFi SSID",
|
||||
"filter_bssid": "Filtro por WiFi BSSID",
|
||||
"bssid_list": "Lista de WiFi BSSID",
|
||||
"filter_did": "Filtrar Dispositivos",
|
||||
"did_list": "Lista de Dispositivos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Falha em conectar o dispositivo",
|
||||
"cannot_login": "Falha no login com a conta xiaomi, verifique a senha. Se está tendo falhas ao fazer o login, faça login em mi.com na mesma rede atual e tente novamente.",
|
||||
"cannot_reach": "Incapaz de accessar a API Xiaomi",
|
||||
"tzinfo_error": "Acesse: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Sem dispositivos na conta xiaomi"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Adicionar nova integração](/_my_redirect/config_flow_start?domain=xiaomi_miot) para modificar a customização.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Versião do componente",
|
||||
"can_reach_server": "Acessar server Xiaomi API",
|
||||
"can_reach_spec": "Acessar server MIoT-Spec",
|
||||
"logged_accounts": "Contas logadas",
|
||||
"total_devices": "Total de dispositivos MiHome"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"password": "Senha",
|
||||
"biological": "Biologico",
|
||||
"key": "Chave",
|
||||
"turntable": "Giratória",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "Senha de Única Vez",
|
||||
"two_step_verification": "Verificação de Dois Passos",
|
||||
"coercion": "Coerção",
|
||||
"homekit": "Homekit",
|
||||
"manual": "Manual",
|
||||
"automatic": "Automatico"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "Desbloquear de fora",
|
||||
"lock": "Bloquear",
|
||||
"anti_lock_on": "Habilitar anti-bloqueio",
|
||||
"anti_lock_off": "Desabilitar anti-bloqueio",
|
||||
"inside_unlock": "Desbloquear de dentro",
|
||||
"lock_inside": "Bloquear de dentro",
|
||||
"child_lock_on": "Habilitar bloqueio por criança",
|
||||
"child_lock_off": "Desabilitar bloqueio por criança",
|
||||
"lock_outside": "Bloquear de fora"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "Aberto",
|
||||
"close": "Fechado",
|
||||
"close_timeout": "Timeout de Fechamento",
|
||||
"knock": "Batido",
|
||||
"breaking": "Arrombado",
|
||||
"stuck": "Travado"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Auto",
|
||||
"low": "Baixo",
|
||||
"medium": "Medio",
|
||||
"middle": "Meio",
|
||||
"high": "Alto",
|
||||
"quiet": "Silencioso",
|
||||
"turbo": "Turbo",
|
||||
"level1": "Nível 1",
|
||||
"level2": "Nível 2",
|
||||
"level3": "Nível 3",
|
||||
"level4": "Nível 4",
|
||||
"level5": "Nível 5",
|
||||
"level6": "Nível 6",
|
||||
"level7": "Nível 7"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Desligado",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"both": "Ambos"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"name": "Enviar comando",
|
||||
"description": "Enviar comando miio",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Metodo",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "Parâmetros",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_properties": {
|
||||
"name": "Obter propriedades",
|
||||
"description": "Obtem as propriedades miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": ""
|
||||
},
|
||||
"mapping": {
|
||||
"name": "Mapeamento",
|
||||
"description": ""
|
||||
},
|
||||
"update_entity": {
|
||||
"name": "Atualizar entidade",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_property": {
|
||||
"name": "Definir propriedade",
|
||||
"description": "Define a propriedade miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": ""
|
||||
},
|
||||
"field": {
|
||||
"name": "Field",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "Value",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_miot_property": {
|
||||
"name": "Definir propriedade miot",
|
||||
"description": "Define a propriedade miot por siid/piid",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"piid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"call_action": {
|
||||
"name": "Chamar ação",
|
||||
"description": "Chama uma ação miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"aiid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"name": "Caixa de som inteligenter",
|
||||
"description": "Caixa de som inteligente para XiaoAi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": "ID da Entidade Xiaoai"
|
||||
},
|
||||
"text": {
|
||||
"name": "Texto",
|
||||
"description": ""
|
||||
},
|
||||
"execute": {
|
||||
"name": "Executar",
|
||||
"description": ""
|
||||
},
|
||||
"silent": {
|
||||
"name": "Silenciar",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"name": "Despertar XiaoA",
|
||||
"description": "Despertar a caixa de som XiaoAi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": "ID da Entidade Xiaoai"
|
||||
},
|
||||
"text": {
|
||||
"name": "Texto",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_device_data": {
|
||||
"name": "Obter dados do dispositivo",
|
||||
"description": "Obtem dados do dispositivo pela cloud",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"name": "Tipo",
|
||||
"description": ""
|
||||
},
|
||||
"key": {
|
||||
"name": "Chave",
|
||||
"description": ""
|
||||
},
|
||||
"time_start": {
|
||||
"name": "Hora de início",
|
||||
"description": ""
|
||||
},
|
||||
"time_end": {
|
||||
"name": "Hora de finalização",
|
||||
"description": ""
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limite",
|
||||
"description": ""
|
||||
},
|
||||
"group": {
|
||||
"name": "Gropo",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"renew_devices": {
|
||||
"name": "Renovar dispositivos",
|
||||
"description": "Renovar dispositivos xiaomi do cache",
|
||||
"fields": {
|
||||
"username": {
|
||||
"name": "Nome de usuário",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_token": {
|
||||
"name": "Obter token",
|
||||
"description": "Obtem o token do dispositivo xiaomi",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Nome",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_bindkey": {
|
||||
"name": "Obter bindkey",
|
||||
"description": "Obtem o bindkey para dispositivo ble",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": ""
|
||||
},
|
||||
"did": {
|
||||
"name": "DID",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"name": "Solicitar API xiaomi",
|
||||
"description": "Solicita API xiaomi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "ID da Entidade",
|
||||
"description": ""
|
||||
},
|
||||
"api": {
|
||||
"name": "API",
|
||||
"description": ""
|
||||
},
|
||||
"data": {
|
||||
"name": "Dado",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Metodo",
|
||||
"description": ""
|
||||
},
|
||||
"crypt": {
|
||||
"name": "Crypt",
|
||||
"description": ""
|
||||
},
|
||||
"sid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Выберите действие",
|
||||
"data": {
|
||||
"action": "Действие"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Конфигурация устройства.",
|
||||
"data": {
|
||||
"host": "Локальный IP-адрес",
|
||||
"token": "Токен",
|
||||
"name": "Имя устройства",
|
||||
"scan_interval": "Интервал обновления (секунды)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Настройка облачного аккаунта Сяоми.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Account ID / Email",
|
||||
"password": "Пароль",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Регион сервера MiCloud",
|
||||
"conn_mode": "Режим подключения устройства",
|
||||
"filter_models": "Фильтровать устройства по модели/семья/WiFi (Продвинутые настр.)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Фильтрация облачных устройств",
|
||||
"description": "Вы можете включать или исключать устройства которые хотите интегрировать. В режиме исключения, если не выбрать ни одного устройства - добавятся все.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Режим фильтрации по устройствам",
|
||||
"model_list": "Список модели устройств",
|
||||
"filter_ssid": "Фильтр WiFi SSID",
|
||||
"home_id_list": "Домашний список",
|
||||
"ssid_list": "Список WiFi SSID",
|
||||
"filter_bssid": "Фильтр WiFi BSSID",
|
||||
"bssid_list": "Список WiFi BSSID",
|
||||
"filter_did": "Фильтр устройств",
|
||||
"did_list": "Список устройств"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Изменение сущностей / устройств",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Выберите домен сущности",
|
||||
"entity": "Выберите сущность",
|
||||
"model": "Выберите модель устройств",
|
||||
"model_specified": "Особая модель устройства (Дополнительно)",
|
||||
"only_main_entity": "Только основные (родительские) сущности",
|
||||
"reset_customizes": "Сбросить настройки по умолчанию",
|
||||
"bool2selects": "Настройка опций"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Не удалось подключиться к устройству",
|
||||
"cannot_login": "Не удалось подключиться к аккаунту Сяоми. Проверьте пароль. При повторных ошибках авторизируйтесь на mi.com в той же среде и попробуйте снова.",
|
||||
"cannot_reach": "Нет доступа к API Сяоми",
|
||||
"tzinfo_error": "Перейдите по ссылке: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Не найдено устройств в облаке Сяоми"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Конфигурация сохранена! [Настроить снова](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nНекоторые опции требуют перезагрузки конфигурации [reload integration](/developer-tools/yaml) для эффекта.",
|
||||
"already_configured": "Устройство уже сконфигурировано",
|
||||
"not_xiaomi_miio": "Не является устройством miio/miot."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Конфигурация устройства.",
|
||||
"data": {
|
||||
"host": "Локальный IP-адрес",
|
||||
"token": "Токен",
|
||||
"scan_interval": "Интервал обновления (секунды)",
|
||||
"miot_cloud": "Enable miot cloud (Config account in YAML)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Конфигурация облачного аккаунта Сяоми.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Account ID / Email",
|
||||
"password": "Пароль",
|
||||
"captcha": "Captcha",
|
||||
"server_country": "Регион сервера MiCloud",
|
||||
"conn_mode": "Режим подключения устройств",
|
||||
"renew_devices": "Заставлять устройства обновляться",
|
||||
"disable_message": "Выключить датчик уведомлений от Mi Home"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Фильтрация облачных устройств",
|
||||
"description": "Вы можете включать или исключать устройства которые хотите интегрировать. В режиме исключения, если не выбрать ни одного устройства - добавятся все.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Режим фильтрации по устройствам",
|
||||
"model_list": "Список модели устройств",
|
||||
"filter_ssid": "Фильтр WiFi SSID",
|
||||
"ssid_list": "Список WiFi SSID",
|
||||
"filter_bssid": "Фильтр WiFi BSSID",
|
||||
"bssid_list": "СписокWiFi BSSID",
|
||||
"filter_did": "Фильтр устройств",
|
||||
"did_list": "Список устройств"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Не удалость подключиться к устройству",
|
||||
"cannot_login": "Не удалось полключиться к аккаунту Сяоми. Проверьте пароль. При повторных ошибках авторизируйтесь на mi.com в той же среде и попробуйте снова.",
|
||||
"cannot_reach": "Нет доступа к API Сяоми",
|
||||
"tzinfo_error": "Перейдите по ссылке: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Не найдено устройств в облаке Сяоми"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Add a new integration](/_my_redirect/config_flow_start?domain=xiaomi_miot) to modify customization.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Версия компонента",
|
||||
"can_reach_server": "Доступность API сервера Сяоми",
|
||||
"can_reach_spec": "Доступность MIoT-Spec сервера",
|
||||
"logged_accounts": "Авторизованные аккаунты",
|
||||
"total_devices": "Общее количество устройств MiHome"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Eylem Seçin",
|
||||
"data": {
|
||||
"action": "Eylem"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Cihaz bilgilerini yapılandırın.",
|
||||
"data": {
|
||||
"host": "Yerel IP",
|
||||
"token": "Token",
|
||||
"name": "Cihaz adı",
|
||||
"scan_interval": "Durum güncelleme aralığı (saniye)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Xiaomi bulut hesabını yapılandırın.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Hesap ID / E-posta",
|
||||
"password": "Şifre",
|
||||
"captcha": "Doğrulama Kodu",
|
||||
"verify_ticket": "İki faktörlü doğrulama kodu",
|
||||
"server_country": "MiCloud sunucu konumu",
|
||||
"conn_mode": "Cihaz bağlantı modu",
|
||||
"trans_options": "Özellik değeri açıklamasını çevir",
|
||||
"filter_models": "Cihazları model/ev/WiFi ile filtrele (Gelişmiş)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Bulut cihazlarını filtrele",
|
||||
"description": "Yalnızca istediğiniz cihazları entegre etmek için cihazları dahil edebilir veya hariç tutabilirsiniz.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Cihaz Modelini Filtrele",
|
||||
"model_list": "Cihaz Model Listesi",
|
||||
"filter_home_id": "Evi Filtrele",
|
||||
"home_id_list": "Ev Listesi",
|
||||
"filter_ssid": "WiFi SSID Filtrele",
|
||||
"ssid_list": "WiFi SSID Listesi",
|
||||
"filter_bssid": "WiFi BSSID Filtrele",
|
||||
"bssid_list": "WiFi BSSID Listesi",
|
||||
"filter_did": "Cihazı Filtrele",
|
||||
"did_list": "Cihaz Listesi",
|
||||
"home_ids": "Evi Filtrele (Kaydetmek için boş bırakın)"
|
||||
},
|
||||
"data_description": {
|
||||
"filter_did": "Hariç tutma modunda, listeden herhangi bir seçenek işaretlemezseniz, tüm cihazları dahil etmekle eşdeğerdir.",
|
||||
"home_ids": "Bu seçeneği işaretleyip gönderdikten sonra mevcut sayfaya döneceksiniz,\ncihaz listesi yenilenecek ve işaretlenen evlerin cihazları filtrelenecektir."
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Varlıkları / cihazları özelleştirme",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Varlık alanını seçin",
|
||||
"entity": "Varlık seçin",
|
||||
"model": "Cihaz modelini seçin",
|
||||
"model_specified": "Belirli cihaz modeli (İsteğe bağlı)",
|
||||
"only_main_entity": "Yalnızca ana (üst) varlıklar",
|
||||
"yaml_mode": "YAML Modu",
|
||||
"yaml_customizes": "Özelleştirmeler YAML Kodu",
|
||||
"reset_customizes": "Varsayılan özelleştirmelere sıfırla",
|
||||
"bool2selects": "Seçenekleri özelleştir"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Cihaza bağlanılamadı",
|
||||
"cannot_login": "Xiaomi hesabına giriş yapılamadı. Lütfen hesap bilgilerinizi kontrol edin. Giriş denemeleri birden fazla kez başarısız olursa, lütfen aynı ağ ortamında Xiaomi resmi web sitesinden (mi.com) tekrar giriş yapmayı deneyin.",
|
||||
"cannot_reach": "Xiaomi API arayüzüne erişilemiyor",
|
||||
"tzinfo_error": "Hass ortamınızda bir sorun var gibi görünüyor. Lütfen şu adrese bakın: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Bu Xiaomi hesabında cihaz bulunamadı",
|
||||
"need_verify": "Bu giriş güvenlik doğrulaması gerektiriyor. Lütfen doğrulama sayfasını açarak mobil/e-posta doğrulama kodunu gönderin. Doğrulama kodunu aldıktan sonra web sayfasında doğrulamayın. HomeAssistant'a dönün ve aşağıdaki giriş kutusuna girin."
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Yapılandırma kaydedildi! [Tekrar özelleştir](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nBazı özel seçeneklerin etkili olması için [entegrasyonun yeniden yüklenmesi](/developer-tools/yaml) gerekebilir.",
|
||||
"already_configured": "Bu cihaz zaten yapılandırılmış.",
|
||||
"not_xiaomi_miio": "Bu cihaz miio/miot protokolünü desteklemiyor."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Cihaz bilgilerini yapılandırın.",
|
||||
"data": {
|
||||
"host": "Cihaz IP",
|
||||
"token": "Token",
|
||||
"scan_interval": "Durum güncelleme aralığı (saniye)",
|
||||
"miot_cloud": "Miot bulutu etkinleştir (Önce YAML'da hesabı yapılandırın)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Xiaomi bulut hesabını yapılandırın.\n{tip}",
|
||||
"data": {
|
||||
"username": "Xiaomi Hesap ID / E-posta",
|
||||
"password": "Şifre",
|
||||
"captcha": "Doğrulama Kodu",
|
||||
"verify_ticket": "İki faktörlü doğrulama kodu",
|
||||
"server_country": "MiCloud sunucu konumu",
|
||||
"conn_mode": "Cihaz bağlantı modu",
|
||||
"renew_devices": "Cihazları zorla yenile",
|
||||
"trans_options": "Özellik değeri açıklamasını çevir",
|
||||
"disable_message": "Mihome bildirim sensörünü devre dışı bırak",
|
||||
"disable_scene_history": "Mihome sahne geçmişi sensörünü devre dışı bırak"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Bulut cihazlarını filtrele",
|
||||
"description": "Yalnızca istediğiniz cihazları entegre etmek için cihazları dahil edebilir veya hariç tutabilirsiniz.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Cihaz Modelini Filtrele",
|
||||
"model_list": "Cihaz Model Listesi",
|
||||
"filter_home_id": "Evi Filtrele",
|
||||
"home_id_list": "Ev Listesi",
|
||||
"filter_ssid": "WiFi SSID Filtrele",
|
||||
"ssid_list": "WiFi SSID Listesi",
|
||||
"filter_bssid": "WiFi BSSID Filtrele",
|
||||
"bssid_list": "WiFi BSSID Listesi",
|
||||
"filter_did": "Cihazı Filtrele",
|
||||
"did_list": "Cihaz Listesi",
|
||||
"home_ids": "Evi Filtrele (Kaydetmek için boş bırakın)"
|
||||
},
|
||||
"data_description": {
|
||||
"filter_did": "Hariç tutma modunda, listeden herhangi bir seçenek işaretlemezseniz, tüm cihazları dahil etmekle eşdeğerdir.",
|
||||
"home_ids": "Bu seçeneği işaretleyip gönderdikten sonra mevcut sayfaya döneceksiniz,\ncihaz listesi yenilenecek ve işaretlenen evlerin cihazları filtrelenecektir."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Cihaza bağlanılamadı",
|
||||
"cannot_login": "Xiaomi hesabına giriş yapılamadı. Lütfen hesap bilgilerinizi kontrol edin. Giriş denemeleri birden fazla kez başarısız olursa, lütfen aynı ağ ortamında Xiaomi resmi web sitesinden (mi.com) tekrar giriş yapmayı deneyin.",
|
||||
"cannot_reach": "Xiaomi API arayüzüne erişilemiyor",
|
||||
"tzinfo_error": "Hass ortamınızda bir sorun var gibi görünüyor. Lütfen şu adrese bakın: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Bu Xiaomi hesabında cihaz bulunamadı",
|
||||
"need_verify": "Bu giriş güvenlik doğrulaması gerektiriyor. Lütfen doğrulama sayfasını açarak mobil/e-posta doğrulama kodunu gönderin. Doğrulama kodunu aldıktan sonra web sayfasında doğrulamayın. HomeAssistant'a dönün ve aşağıdaki giriş kutusuna girin."
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\nÖzelleştirmeyi değiştirmek için [yeni bir entegrasyon ekleyin](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Özel bileşen sürümü",
|
||||
"can_reach_server": "Xiaomi API sunucusuna erişim",
|
||||
"can_reach_spec": "MIoT-Spec Sunucusuna erişim",
|
||||
"logged_accounts": "Giriş yapılmış hesap sayısı",
|
||||
"total_devices": "Toplam MiHome cihaz sayısı"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"info": {"name": "Bilgi"}
|
||||
},
|
||||
"sensor": {
|
||||
"clean_area": {"name": "Temizlenen Alan"},
|
||||
"clean_time": {"name": "Temizlik Süresi"},
|
||||
"power_cost_today": {"name": "Bugünkü Enerji"},
|
||||
"power_cost_month": {"name": "Aylık Enerji"},
|
||||
"power_cost_today_2": {"name": "Bugünkü Enerji"},
|
||||
"power_cost_month_2": {"name": "Aylık Enerji"},
|
||||
"props-clean_area": {"name": "Temizlenen Alan"},
|
||||
"props-clean_time": {"name": "Temizlik Süresi"},
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"password": "Şifre",
|
||||
"biological": "Biyometrik",
|
||||
"key": "Anahtar",
|
||||
"turntable": "Döner Kadran",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "Tek Kullanımlık Şifre",
|
||||
"two_step_verification": "İki Adımlı Doğrulama",
|
||||
"coercion": "Zorlama",
|
||||
"homekit": "Homekit",
|
||||
"manual": "Manuel",
|
||||
"automatic": "Otomatik"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "Dışarıdan kilidi aç",
|
||||
"lock": "Kilitle",
|
||||
"anti_lock_on": "Anti-kilidi etkinleştir",
|
||||
"anti_lock_off": "Anti-kilidi devre dışı bırak",
|
||||
"inside_unlock": "İçeriden kilidi aç",
|
||||
"lock_inside": "İçeriden kilitle",
|
||||
"child_lock_on": "Çocuk kilidini etkinleştir",
|
||||
"child_lock_off": "Çocuk kilidini devre dışı bırak",
|
||||
"lock_outside": "Dışarıdan kilitle"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "Açık",
|
||||
"close": "Kapalı",
|
||||
"close_timeout": "Kapanma zaman aşımı",
|
||||
"knock": "Kapı çalındı",
|
||||
"breaking": "Zorla giriş",
|
||||
"stuck": "Sıkışmış"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Otomatik",
|
||||
"low": "Düşük",
|
||||
"medium": "Orta",
|
||||
"middle": "Orta",
|
||||
"high": "Yüksek",
|
||||
"quiet": "Sessiz",
|
||||
"turbo": "Turbo",
|
||||
"level1": "Seviye 1",
|
||||
"level2": "Seviye 2",
|
||||
"level3": "Seviye 3",
|
||||
"level4": "Seviye 4",
|
||||
"level5": "Seviye 5",
|
||||
"level6": "Seviye 6",
|
||||
"level7": "Seviye 7"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Kapalı",
|
||||
"vertical": "Dikey",
|
||||
"horizontal": "Yatay",
|
||||
"both": "Her İkisi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"name": "Komut gönder",
|
||||
"description": "Miio komutu gönder",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Metod",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "Parametreler",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_properties": {
|
||||
"name": "Özellikleri al",
|
||||
"description": "Miot özelliklerini al",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": ""
|
||||
},
|
||||
"mapping": {
|
||||
"name": "Eşleme",
|
||||
"description": ""
|
||||
},
|
||||
"update_entity": {
|
||||
"name": "Varlığı güncelle",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_property": {
|
||||
"name": "Özellik ayarla",
|
||||
"description": "Miot özelliğini ayarla",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": ""
|
||||
},
|
||||
"field": {
|
||||
"name": "Alan",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "Değer",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_miot_property": {
|
||||
"name": "Miot özelliği ayarla",
|
||||
"description": "siid/piid ile miot özelliğini ayarla",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"piid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"call_action": {
|
||||
"name": "Eylem çağır",
|
||||
"description": "Miot eylemini çağır",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"aiid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"name": "Akıllı hoparlör",
|
||||
"description": "XiaoAi için akıllı hoparlör",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": "XiaoAi Varlık ID"
|
||||
},
|
||||
"text": {
|
||||
"name": "Metin",
|
||||
"description": ""
|
||||
},
|
||||
"execute": {
|
||||
"name": "Çalıştır",
|
||||
"description": ""
|
||||
},
|
||||
"silent": {
|
||||
"name": "Sessiz",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"name": "XiaoAi Uyandır",
|
||||
"description": "XiaoAi hoparlörünü uyandır",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": "XiaoAi Varlık ID"
|
||||
},
|
||||
"text": {
|
||||
"name": "Metin",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_device_data": {
|
||||
"name": "Cihaz verisini al",
|
||||
"description": "Buluttan xiaomi cihaz verisini al",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"name": "Tür",
|
||||
"description": ""
|
||||
},
|
||||
"key": {
|
||||
"name": "Anahtar",
|
||||
"description": ""
|
||||
},
|
||||
"time_start": {
|
||||
"name": "Başlangıç zamanı",
|
||||
"description": ""
|
||||
},
|
||||
"time_end": {
|
||||
"name": "Bitiş zamanı",
|
||||
"description": ""
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": ""
|
||||
},
|
||||
"group": {
|
||||
"name": "Grup",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"renew_devices": {
|
||||
"name": "Cihazları yenile",
|
||||
"description": "Önbelleğe alınmış xiaomi cihazlarını yenile",
|
||||
"fields": {
|
||||
"username": {
|
||||
"name": "Kullanıcı adı",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_token": {
|
||||
"name": "Token al",
|
||||
"description": "Xiaomi cihaz tokenını al",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Ad",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_bindkey": {
|
||||
"name": "Bindkey al",
|
||||
"description": "BLE cihazı için bindkey al",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": ""
|
||||
},
|
||||
"did": {
|
||||
"name": "DID",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"name": "Xiaomi API isteği",
|
||||
"description": "Xiaomi API'sine istek gönder",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Varlık ID",
|
||||
"description": ""
|
||||
},
|
||||
"api": {
|
||||
"name": "API",
|
||||
"description": ""
|
||||
},
|
||||
"data": {
|
||||
"name": "Veri",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Metod",
|
||||
"description": ""
|
||||
},
|
||||
"crypt": {
|
||||
"name": "Şifreleme",
|
||||
"description": ""
|
||||
},
|
||||
"sid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Chọn hành động",
|
||||
"data": {
|
||||
"action": "Hành động"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Cấu hình thông tin thiết bị.",
|
||||
"data": {
|
||||
"host": "IP cục bộ",
|
||||
"token": "Token",
|
||||
"name": "Tên thiết bị",
|
||||
"scan_interval": "Chu kỳ cập nhật trạng thái (giây)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Cấu hình tài khoản Xiaomi Cloud.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID tài khoản Xiaomi / Email",
|
||||
"password": "Mật khẩu",
|
||||
"captcha": "Captcha",
|
||||
"verify_ticket": "Mã xác thực hai bước",
|
||||
"server_country": "Vị trí máy chủ MiCloud",
|
||||
"conn_mode": "Chế độ kết nối thiết bị",
|
||||
"trans_options": "Mô tả dịch thuộc tính",
|
||||
"filter_models": "Lọc thiết bị theo model/home/WiFi (Nâng cao)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Lọc thiết bị cloud",
|
||||
"description": "Bạn có thể bao gồm hoặc loại trừ thiết bị để chỉ tích hợp thiết bị mong muốn.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Lọc theo model thiết bị",
|
||||
"model_list": "Danh sách model thiết bị",
|
||||
"filter_home_id": "Lọc theo Home",
|
||||
"home_id_list": "Danh sách Home",
|
||||
"filter_ssid": "Lọc theo WiFi SSID",
|
||||
"ssid_list": "Danh sách WiFi SSID",
|
||||
"filter_bssid": "Lọc theo WiFi BSSID",
|
||||
"bssid_list": "Danh sách WiFi BSSID",
|
||||
"filter_did": "Lọc theo thiết bị",
|
||||
"did_list": "Danh sách thiết bị",
|
||||
"home_ids": "Lọc theo Home (Để trống để lưu)"
|
||||
},
|
||||
"data_description": {
|
||||
"filter_did": "Ở chế độ loại trừ, nếu không chọn mục nào, sẽ bao gồm tất cả thiết bị.",
|
||||
"home_ids": "Sau khi chọn và gửi, bạn sẽ quay lại trang này, danh sách thiết bị sẽ làm mới và lọc theo Home đã chọn."
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "Tùy chỉnh thực thể / thiết bị",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "Chọn domain thực thể",
|
||||
"entity": "Chọn thực thể",
|
||||
"model": "Chọn model thiết bị",
|
||||
"model_specified": "Model thiết bị chỉ định (Tùy chọn)",
|
||||
"only_main_entity": "Chỉ thực thể chính (cha)",
|
||||
"yaml_mode": "Chế độ YAML",
|
||||
"yaml_customizes": "Mã YAML tùy chỉnh",
|
||||
"reset_customizes": "Khôi phục tùy chỉnh mặc định",
|
||||
"bool2selects": "Tùy chọn tùy chỉnh"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Không thể kết nối tới thiết bị",
|
||||
"cannot_login": "Đăng nhập tài khoản Xiaomi thất bại. Vui lòng kiểm tra thông tin đăng nhập. Nếu thử nhiều lần vẫn không được, hãy thử đăng nhập lại trên trang chính thức Xiaomi (mi.com) cùng mạng.",
|
||||
"cannot_reach": "Không thể truy cập API Xiaomi",
|
||||
"tzinfo_error": "Có vẻ môi trường Hass của bạn gặp vấn đề. Xem: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Không tìm thấy thiết bị nào trong tài khoản Xiaomi này",
|
||||
"need_verify": "Đăng nhập này cần xác thực bảo mật. Hãy mở trang xác thực để gửi mã xác minh qua điện thoại/email. Sau khi nhận mã, KHÔNG xác minh trên web mà quay lại Home Assistant để nhập mã vào ô bên dưới."
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "Đã lưu cấu hình! [Tùy chỉnh lại](/_my_redirect/config_flow_start?domain=xiaomi_miot).\n{tip}\n\nMột số tùy chọn cần [tải lại tích hợp](/developer-tools/yaml) để có hiệu lực.",
|
||||
"already_configured": "Thiết bị này đã được cấu hình.",
|
||||
"not_xiaomi_miio": "Thiết bị này không hỗ trợ giao thức miio/miot."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "Cấu hình thông tin thiết bị.",
|
||||
"data": {
|
||||
"host": "IP thiết bị",
|
||||
"token": "Token",
|
||||
"scan_interval": "Chu kỳ cập nhật trạng thái (giây)",
|
||||
"miot_cloud": "Bật miot cloud (Cấu hình tài khoản trong YAML trước)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "Cấu hình tài khoản Xiaomi Cloud.\n{tip}",
|
||||
"data": {
|
||||
"username": "ID tài khoản Xiaomi / Email",
|
||||
"password": "Mật khẩu",
|
||||
"captcha": "Captcha",
|
||||
"verify_ticket": "Mã xác thực hai bước",
|
||||
"server_country": "Vị trí máy chủ MiCloud",
|
||||
"conn_mode": "Chế độ kết nối thiết bị",
|
||||
"renew_devices": "Làm mới danh sách thiết bị",
|
||||
"trans_options": "Mô tả dịch thuộc tính",
|
||||
"disable_message": "Tắt cảm biến thông báo Mihome",
|
||||
"disable_scene_history": "Tắt cảm biến lịch sử cảnh Mihome"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "Lọc thiết bị cloud",
|
||||
"description": "Bạn có thể bao gồm hoặc loại trừ thiết bị để chỉ tích hợp thiết bị mong muốn.\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "Lọc theo model thiết bị",
|
||||
"model_list": "Danh sách model thiết bị",
|
||||
"filter_home_id": "Lọc theo Home",
|
||||
"home_id_list": "Danh sách Home",
|
||||
"filter_ssid": "Lọc theo WiFi SSID",
|
||||
"ssid_list": "Danh sách WiFi SSID",
|
||||
"filter_bssid": "Lọc theo WiFi BSSID",
|
||||
"bssid_list": "Danh sách WiFi BSSID",
|
||||
"filter_did": "Lọc theo thiết bị",
|
||||
"did_list": "Danh sách thiết bị",
|
||||
"home_ids": "Lọc theo Home (Để trống để lưu)"
|
||||
},
|
||||
"data_description": {
|
||||
"filter_did": "Ở chế độ loại trừ, nếu không chọn mục nào, sẽ bao gồm tất cả thiết bị.",
|
||||
"home_ids": "Sau khi chọn và gửi, bạn sẽ quay lại trang này, danh sách thiết bị sẽ làm mới và lọc theo Home đã chọn."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Không thể kết nối tới thiết bị",
|
||||
"cannot_login": "Đăng nhập tài khoản Xiaomi thất bại. Vui lòng kiểm tra thông tin đăng nhập. Nếu thử nhiều lần vẫn không được, hãy thử đăng nhập lại trên trang chính thức Xiaomi (mi.com) cùng mạng.",
|
||||
"cannot_reach": "Không thể truy cập API Xiaomi",
|
||||
"tzinfo_error": "Có vẻ môi trường Hass của bạn gặp vấn đề. Xem: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "Không tìm thấy thiết bị nào trong tài khoản Xiaomi này",
|
||||
"need_verify": "Đăng nhập này cần xác thực bảo mật. Hãy mở trang xác thực để gửi mã xác minh qua điện thoại/email. Sau khi nhận mã, KHÔNG xác minh trên web mà quay lại Home Assistant để nhập mã vào ô bên dưới."
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n[Thêm tích hợp mới](/_my_redirect/config_flow_start?domain=xiaomi_miot) để chỉnh sửa tùy chỉnh.\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "Phiên bản custom component",
|
||||
"can_reach_server": "Kết nối tới máy chủ API Xiaomi",
|
||||
"can_reach_spec": "Kết nối tới máy chủ MIoT-Spec",
|
||||
"logged_accounts": "Số tài khoản đã đăng nhập",
|
||||
"total_devices": "Tổng số thiết bị MiHome"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"info": {"name": "Thông tin"}
|
||||
},
|
||||
"sensor": {
|
||||
"clean_area": {"name": "Diện tích đã làm sạch"},
|
||||
"clean_time": {"name": "Thời gian làm sạch"},
|
||||
"power_cost_today": {"name": "Năng lượng hôm nay"},
|
||||
"power_cost_month": {"name": "Năng lượng tháng này"},
|
||||
"power_cost_today_2": {"name": "Năng lượng hôm nay"},
|
||||
"power_cost_month_2": {"name": "Năng lượng tháng này"},
|
||||
"props-clean_area": {"name": "Diện tích đã làm sạch"},
|
||||
"props-clean_time": {"name": "Thời gian làm sạch"},
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"password": "Mật khẩu",
|
||||
"biological": "Sinh trắc",
|
||||
"key": "Chìa khóa",
|
||||
"turntable": "Vặn tay",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "Mật khẩu 1 lần",
|
||||
"two_step_verification": "Xác thực 2 bước",
|
||||
"coercion": "Cưỡng chế",
|
||||
"homekit": "Homekit",
|
||||
"manual": "Thủ công",
|
||||
"automatic": "Tự động"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "Mở khóa từ ngoài",
|
||||
"lock": "Khóa",
|
||||
"anti_lock_on": "Bật chống khóa",
|
||||
"anti_lock_off": "Tắt chống khóa",
|
||||
"inside_unlock": "Mở khóa từ trong",
|
||||
"lock_inside": "Khóa từ trong",
|
||||
"child_lock_on": "Bật khóa trẻ em",
|
||||
"child_lock_off": "Tắt khóa trẻ em",
|
||||
"lock_outside": "Khóa từ ngoài"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "Mở",
|
||||
"close": "Đóng",
|
||||
"close_timeout": "Đóng quá thời gian",
|
||||
"knock": "Gõ cửa",
|
||||
"breaking": "Phá cửa",
|
||||
"stuck": "Kẹt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "Tự động",
|
||||
"low": "Thấp",
|
||||
"medium": "Trung bình",
|
||||
"middle": "Trung bình",
|
||||
"high": "Cao",
|
||||
"quiet": "Yên lặng",
|
||||
"turbo": "Turbo",
|
||||
"level1": "Mức 1",
|
||||
"level2": "Mức 2",
|
||||
"level3": "Mức 3",
|
||||
"level4": "Mức 4",
|
||||
"level5": "Mức 5",
|
||||
"level6": "Mức 6",
|
||||
"level7": "Mức 7"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "Tắt",
|
||||
"vertical": "Dọc",
|
||||
"horizontal": "Ngang",
|
||||
"both": "Cả hai"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"name": "Gửi lệnh",
|
||||
"description": "Gửi lệnh miio",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Phương thức",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "Tham số",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_properties": {
|
||||
"name": "Lấy thuộc tính",
|
||||
"description": "Lấy thuộc tính miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"mapping": {
|
||||
"name": "Ánh xạ",
|
||||
"description": ""
|
||||
},
|
||||
"update_entity": {
|
||||
"name": "Cập nhật thực thể",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_property": {
|
||||
"name": "Đặt thuộc tính",
|
||||
"description": "Đặt thuộc tính miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"field": {
|
||||
"name": "Trường",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "Giá trị",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_miot_property": {
|
||||
"name": "Đặt thuộc tính miot",
|
||||
"description": "Đặt thuộc tính miot theo siid/piid",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"piid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"value": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"call_action": {
|
||||
"name": "Gọi hành động",
|
||||
"description": "Gọi action miot",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"siid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"aiid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
},
|
||||
"params": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"name": "Loa thông minh",
|
||||
"description": "Loa thông minh cho XiaoAi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Xiaoai Entity ID"
|
||||
},
|
||||
"text": {
|
||||
"name": "Văn bản",
|
||||
"description": ""
|
||||
},
|
||||
"execute": {
|
||||
"name": "Thực thi",
|
||||
"description": ""
|
||||
},
|
||||
"silent": {
|
||||
"name": "Im lặng",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"name": "Đánh thức XiaoAi",
|
||||
"description": "Đánh thức loa XiaoAi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Xiaoai Entity ID"
|
||||
},
|
||||
"text": {
|
||||
"name": "Văn bản",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_device_data": {
|
||||
"name": "Lấy dữ liệu thiết bị",
|
||||
"description": "Lấy dữ liệu thiết bị xiaomi từ cloud",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"name": "Loại",
|
||||
"description": ""
|
||||
},
|
||||
"key": {
|
||||
"name": "Khóa",
|
||||
"description": ""
|
||||
},
|
||||
"time_start": {
|
||||
"name": "Thời gian bắt đầu",
|
||||
"description": ""
|
||||
},
|
||||
"time_end": {
|
||||
"name": "Thời gian kết thúc",
|
||||
"description": ""
|
||||
},
|
||||
"limit": {
|
||||
"name": "Giới hạn",
|
||||
"description": ""
|
||||
},
|
||||
"group": {
|
||||
"name": "Nhóm",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"renew_devices": {
|
||||
"name": "Làm mới thiết bị",
|
||||
"description": "Làm mới cache thiết bị xiaomi",
|
||||
"fields": {
|
||||
"username": {
|
||||
"name": "Tên đăng nhập",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_token": {
|
||||
"name": "Lấy token",
|
||||
"description": "Lấy token thiết bị xiaomi",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "Tên",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_bindkey": {
|
||||
"name": "Lấy bindkey",
|
||||
"description": "Lấy bindkey cho thiết bị ble",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"did": {
|
||||
"name": "DID",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"name": "Gọi API xiaomi",
|
||||
"description": "Gọi API xiaomi",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": ""
|
||||
},
|
||||
"api": {
|
||||
"name": "API",
|
||||
"description": ""
|
||||
},
|
||||
"data": {
|
||||
"name": "Dữ liệu",
|
||||
"description": ""
|
||||
},
|
||||
"method": {
|
||||
"name": "Phương thức",
|
||||
"description": ""
|
||||
},
|
||||
"crypt": {
|
||||
"name": "Mã hóa",
|
||||
"description": ""
|
||||
},
|
||||
"sid": {
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "选择操作",
|
||||
"data": {
|
||||
"action": "操作"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "配置设备信息",
|
||||
"data": {
|
||||
"host": "设备IP",
|
||||
"token": "Token",
|
||||
"name": "名称",
|
||||
"scan_interval": "更新状态频率(秒)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "配置小米账户\n{tip}",
|
||||
"data": {
|
||||
"username": "小米ID / 邮箱",
|
||||
"password": "小米密码",
|
||||
"captcha": "验证码",
|
||||
"verify_ticket": "邮箱/手机验证码",
|
||||
"server_country": "小米服务器",
|
||||
"conn_mode": "设备连接模式",
|
||||
"trans_options": "翻译属性值描述",
|
||||
"filter_models": "通过型号/家庭/WiFi筛选设备 (高级模式,新手勿选)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "筛选设备",
|
||||
"description": "你可以通过筛选只接入想要的设备。如果想集成网关子设备,请直接选择子设备,接入网关不会自动集成子设备。\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "筛选设备型号",
|
||||
"model_list": "设备型号列表",
|
||||
"filter_home_id": "筛选家庭",
|
||||
"home_id_list": "家庭列表",
|
||||
"filter_ssid": "筛选 WiFi SSID",
|
||||
"ssid_list": "WiFi SSID 列表",
|
||||
"filter_bssid": "筛选 WiFi BSSID",
|
||||
"bssid_list": "WiFi BSSID 列表",
|
||||
"filter_did": "筛选设备",
|
||||
"did_list": "设备列表",
|
||||
"home_ids": "筛选家庭 (留空保存)"
|
||||
},
|
||||
"data_description": {
|
||||
"filter_did": "在排除模式中,如果不勾选任何设备,则相当于接入所有设备",
|
||||
"home_ids": "勾选此项后提交,会回到在当前页面,设备列表会刷新并筛选出已勾选家庭的设备"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "自定义实体 / 设备",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "选择实体类型",
|
||||
"entity": "选择实体",
|
||||
"model": "选择设备型号",
|
||||
"model_specified": "指定设备型号 (可选)",
|
||||
"only_main_entity": "仅主(父)实体",
|
||||
"yaml_mode": "YAML模式",
|
||||
"yaml_customizes": "自定义选项代码",
|
||||
"reset_customizes": "重置为默认自定义",
|
||||
"bool2selects": "自定义选项"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "连接设备失败",
|
||||
"cannot_login": "小米账户登录失败,请检查账号密码。如多次尝试仍登录失败,请在与当前网络相同的环境下登录小米官网(mi.com)后重试",
|
||||
"cannot_reach": "无法访问小米API接口",
|
||||
"tzinfo_error": "你的 Hass 环境看起来有点问题,请参考: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "该小米账户下没有设备",
|
||||
"need_verify": "本次登录需要安全验证,请打开验证网页发送手机/邮箱验证码,收到验证码后不要在网页验证,回到HomeAssistant填入下方输入框"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "配置已保存。\n{tip}\n\n有些自定义选项需要[重载集成](/developer-tools/yaml)才能生效。",
|
||||
"already_configured": "该设备已经配置过",
|
||||
"not_xiaomi_miio": "该设备不支持 miio/miot 协议"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "配置设备信息",
|
||||
"data": {
|
||||
"host": "设备 IP",
|
||||
"token": "Token",
|
||||
"scan_interval": "更新状态频率(秒)",
|
||||
"miot_cloud": "开启云端模式(请先配置账户密码)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "配置小米账户\n{tip}",
|
||||
"data": {
|
||||
"username": "小米ID / 邮箱",
|
||||
"password": "小米密码",
|
||||
"captcha": "验证码",
|
||||
"verify_ticket": "邮箱/手机验证码",
|
||||
"server_country": "小米服务器",
|
||||
"conn_mode": "设备连接模式",
|
||||
"renew_devices": "更新设备列表",
|
||||
"trans_options": "翻译属性值描述",
|
||||
"disable_message": "禁用米家APP通知消息实体",
|
||||
"disable_scene_history": "禁用米家场景历史实体"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "筛选设备",
|
||||
"description": "你可以通过筛选只接入想要的设备。如果想集成网关子设备,请直接选择子设备,接入网关不会自动集成子设备。\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "筛选设备型号",
|
||||
"model_list": "设备型号列表",
|
||||
"filter_home_id": "筛选家庭",
|
||||
"home_id_list": "家庭列表",
|
||||
"filter_ssid": "筛选 WiFi SSID",
|
||||
"ssid_list": "WiFi SSID 列表",
|
||||
"filter_bssid": "筛选 WiFi BSSID",
|
||||
"bssid_list": "WiFi BSSID 列表",
|
||||
"filter_did": "筛选设备",
|
||||
"did_list": "设备列表",
|
||||
"home_ids": "筛选家庭 (留空保存)"
|
||||
},
|
||||
"data_description": {
|
||||
"filter_did": "在排除模式中,如果不勾选任何设备,则相当于接入所有设备",
|
||||
"home_ids": "勾选此项后提交,会回到在当前页面,设备列表会刷新并筛选出已勾选家庭的设备"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "连接设备失败",
|
||||
"cannot_login": "小米账户登录失败,请检查账号密码。如多次尝试仍登录失败,请使用与HA宿主机同一局域网下的其他设备登录小米官网(mi.com)后重试",
|
||||
"cannot_reach": "无法访问小米API接口",
|
||||
"tzinfo_error": "你的Hass环境看起来有点问题,请参考: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "该小米账户下没有设备",
|
||||
"need_verify": "本次登录需要安全验证,请打开验证网页发送手机/邮箱验证码,收到验证码后不要在网页验证,回到HomeAssistant填入下方输入框"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n请再次添加[`Xiaomi Miot`](/_my_redirect/config_flow_start?domain=xiaomi_miot)集成来修改自定义选项\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "自定义组件版本",
|
||||
"can_reach_server": "可访问米家服务器",
|
||||
"can_reach_spec": "可获取MIoT规格",
|
||||
"logged_accounts": "已登录的账户数量",
|
||||
"total_devices": "米家设备总数量"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"info": {"name": "信息"}
|
||||
},
|
||||
"sensor": {
|
||||
"clean_area": {"name": "清洁面积"},
|
||||
"clean_time": {"name": "清洁时间"},
|
||||
"power_cost_today": {"name": "日电量"},
|
||||
"power_cost_month": {"name": "月电量"},
|
||||
"power_cost_today_2": {"name": "日电量"},
|
||||
"power_cost_month_2": {"name": "月电量"},
|
||||
"prop_cal_day-power_cost-today": {"name": "日电量"},
|
||||
"prop_cal_day-power_cost-month": {"name": "月电量"},
|
||||
"props-clean_area": {"name": "清洁面积"},
|
||||
"props-clean_time": {"name": "清洁时间"},
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "蓝牙",
|
||||
"password": "密码",
|
||||
"biological": "生物",
|
||||
"key": "钥匙",
|
||||
"turntable": "转盘",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "一次性密码",
|
||||
"two_step_verification": "双重认证",
|
||||
"coercion": "胁迫",
|
||||
"homekit": "Homekit",
|
||||
"manual": "人工",
|
||||
"automatic": "自动"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "门外开锁",
|
||||
"lock": "上锁",
|
||||
"anti_lock_on": "开启反锁",
|
||||
"anti_lock_off": "解除反锁",
|
||||
"inside_unlock": "门内开锁",
|
||||
"lock_inside": "门内上锁",
|
||||
"child_lock_on": "开启童锁",
|
||||
"child_lock_off": "关闭童锁",
|
||||
"lock_outside": "门外上锁"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "开门",
|
||||
"close": "关门",
|
||||
"close_timeout": "超时未关",
|
||||
"knock": "敲门",
|
||||
"breaking": "撬门",
|
||||
"stuck": "卡住"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "自动",
|
||||
"low": "低档",
|
||||
"medium": "中档",
|
||||
"middle": "中档",
|
||||
"high": "高档",
|
||||
"quiet": "静音",
|
||||
"turbo": "超强",
|
||||
"level1": "一档",
|
||||
"level2": "二档",
|
||||
"level3": "三档",
|
||||
"level4": "四档",
|
||||
"level5": "五档",
|
||||
"level6": "六档",
|
||||
"level7": "七档",
|
||||
"level8": "八档",
|
||||
"leve7": "七档",
|
||||
"leve8": "八档"
|
||||
}
|
||||
},
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"off": "关闭",
|
||||
"auto": "自动",
|
||||
"Heat": "制热",
|
||||
"Warm": "温暖",
|
||||
"Natural Wind": "自然风"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "关闭",
|
||||
"vertical": "垂直",
|
||||
"horizontal": "水平",
|
||||
"both": "立体"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"fan": {
|
||||
"state_attributes": {
|
||||
"preset_modes": {
|
||||
"state": {
|
||||
"Basic Wind": "标准风",
|
||||
"Straight Wind": "直吹风",
|
||||
"Natural Wind": "自然风",
|
||||
"Circular Wind": "循环风",
|
||||
"Baby": "婴儿",
|
||||
"Sleep": "睡眠",
|
||||
"Smart": "智能"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"light": {
|
||||
"state_attributes": {
|
||||
"effect_list": {
|
||||
"state": {
|
||||
"Day": "日光",
|
||||
"Night": "夜光",
|
||||
"Color": "彩光",
|
||||
"Scene": "场景",
|
||||
"Music": "音乐"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"fan_control-fan_level": {
|
||||
"state": {
|
||||
"auto": "自动",
|
||||
"low": "低档",
|
||||
"medium": "中档",
|
||||
"middle": "中档",
|
||||
"high": "高档",
|
||||
"Quiet": "静音",
|
||||
"Turbo": "超强",
|
||||
"Level1": "一档",
|
||||
"Level2": "二档",
|
||||
"Level3": "三档",
|
||||
"Level4": "四档",
|
||||
"Level5": "五档",
|
||||
"Level6": "六档",
|
||||
"Level7": "七档",
|
||||
"Level8": "八档",
|
||||
"Leve7": "七档",
|
||||
"Leve8": "八档"
|
||||
}
|
||||
},
|
||||
"light-mode": {
|
||||
"Day": "日光",
|
||||
"Night": "夜光",
|
||||
"Color": "彩光",
|
||||
"Scene": "场景",
|
||||
"Music": "音乐"
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"humidifier": {
|
||||
"state_attributes": {
|
||||
"available_modes": {
|
||||
"state": {
|
||||
"Off": "关闭",
|
||||
"Sleep": "睡眠",
|
||||
"Strong": "强力",
|
||||
"Constant Humidity": "恒湿"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dehumidifier": {
|
||||
"state_attributes": {
|
||||
"available_modes": {
|
||||
"state": {
|
||||
"Off": "关闭",
|
||||
"Sleep": "睡眠",
|
||||
"Strong": "强力",
|
||||
"Constant Humidity": "恒湿"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"description": "发送miio命令",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体ID"
|
||||
},
|
||||
"method": {
|
||||
"description": "Miio方法"
|
||||
},
|
||||
"params": {
|
||||
"description": "Miio参数"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_properties": {
|
||||
"description": "获取MIoT属性",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体ID"
|
||||
},
|
||||
"mapping": {
|
||||
"description": "MIoT规格"
|
||||
},
|
||||
"update_entity": {
|
||||
"description": "更新实体状态属性"
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_property": {
|
||||
"description": "设置MIoT属性(通过属性名)",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体ID"
|
||||
},
|
||||
"field": {
|
||||
"description": "MIoT属性名"
|
||||
},
|
||||
"value": {
|
||||
"description": "MIoT属性值"
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_miot_property": {
|
||||
"description": "设置MIoT属性(通过siid/piid)",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体ID"
|
||||
},
|
||||
"siid": {
|
||||
"description": "MIoT服务ID"
|
||||
},
|
||||
"piid": {
|
||||
"description": "MIoT属性ID"
|
||||
},
|
||||
"value": {
|
||||
"description": "MIoT属性值"
|
||||
}
|
||||
}
|
||||
},
|
||||
"call_action": {
|
||||
"description": "执行MIoT动作",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体ID"
|
||||
},
|
||||
"siid": {
|
||||
"description": "MIoT服务ID"
|
||||
},
|
||||
"aiid": {
|
||||
"description": "MIoT动作ID"
|
||||
},
|
||||
"params": {
|
||||
"description": "MIoT动作参数"
|
||||
}
|
||||
}
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"description": "小爱TTS及执行语音命令",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "小爱实体ID"
|
||||
},
|
||||
"text": {
|
||||
"description": "文本内容"
|
||||
},
|
||||
"execute": {
|
||||
"description": "执行语音指令,否则朗读文本"
|
||||
},
|
||||
"silent": {
|
||||
"description": "静默执行语音指令"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"description": "唤醒小爱音箱",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "小爱实体ID"
|
||||
},
|
||||
"text": {
|
||||
"description": "文本内容"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_device_data": {
|
||||
"description": "获取设备信息",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体ID"
|
||||
},
|
||||
"type": {
|
||||
"name": "Type",
|
||||
"description": ""
|
||||
},
|
||||
"key": {
|
||||
"name": "Key",
|
||||
"description": ""
|
||||
},
|
||||
"time_start": {
|
||||
"name": "Time start",
|
||||
"description": ""
|
||||
},
|
||||
"time_end": {
|
||||
"name": "Time end",
|
||||
"description": ""
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": ""
|
||||
},
|
||||
"group": {
|
||||
"name": "Group",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"renew_devices": {
|
||||
"description": "刷新设备列表",
|
||||
"fields": {
|
||||
"username": {
|
||||
"name": "小米账号",
|
||||
"description": "小米ID / 手机号 / 邮箱"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_token": {
|
||||
"description": "获取米家设备令牌(token)",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "关键词",
|
||||
"description": "米家APP中的设备名称关键词 / IP / 型号"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_bindkey": {
|
||||
"description": "获取蓝牙设备bindkey",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体ID"
|
||||
},
|
||||
"did": {
|
||||
"description": "设备DID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"description": "请求小米云端接口",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "实体ID"
|
||||
},
|
||||
"api": {
|
||||
"description": "接口地址"
|
||||
},
|
||||
"data": {
|
||||
"description": "请求数据"
|
||||
},
|
||||
"method": {
|
||||
"description": "请求方式"
|
||||
},
|
||||
"crypt": {
|
||||
"description": "是否加密"
|
||||
},
|
||||
"sid": {
|
||||
"description": "小米服务ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Xiaomi Miot",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "選擇操作",
|
||||
"data": {
|
||||
"action": "操作"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "設定裝置資訊",
|
||||
"data": {
|
||||
"host": "裝置 IP",
|
||||
"token": "Token",
|
||||
"name": "名稱",
|
||||
"scan_interval": "狀態更新頻率(秒)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "設定小米帳號\n{tip}",
|
||||
"data": {
|
||||
"username": "小米 ID / 電子郵件",
|
||||
"password": "小米密碼",
|
||||
"captcha": "驗證碼",
|
||||
"server_country": "小米伺服器",
|
||||
"conn_mode": "裝置連線模式",
|
||||
"trans_options": "翻譯屬性值描述",
|
||||
"filter_models": "透過型號/家庭/WiFi篩選裝置 (進階模式,新手勿選)"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "篩選裝置",
|
||||
"description": "你可以透過篩選只接入想要的裝置。在排除模式中,如果不勾選任何裝置,則相當於接入所有裝置。如果想整合網關子裝置,請直接選擇子裝置,接入網關不會自動整合子裝置。\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "篩選裝置型號",
|
||||
"model_list": "裝置型號列表",
|
||||
"filter_home_id": "篩選家庭",
|
||||
"home_id_list": "家庭列表",
|
||||
"filter_ssid": "篩選 WiFi SSID",
|
||||
"ssid_list": "WiFi SSID 列表",
|
||||
"filter_bssid": "篩選 WiFi BSSID",
|
||||
"bssid_list": "WiFi BSSID 列表",
|
||||
"filter_did": "篩選裝置",
|
||||
"did_list": "裝置列表",
|
||||
"home_ids": "篩選家庭 (留白儲存)"
|
||||
}
|
||||
},
|
||||
"customizing": {
|
||||
"title": "自訂實體 / 裝置",
|
||||
"description": "{tip}",
|
||||
"data": {
|
||||
"domain": "選擇實體類型",
|
||||
"entity": "選擇實體",
|
||||
"model": "選擇裝置型號",
|
||||
"model_specified": "指定裝置型號 (可選)",
|
||||
"only_main_entity": "僅主(父)實體",
|
||||
"yaml_mode": "YAML模式",
|
||||
"yaml_customizes": "自訂選項代碼",
|
||||
"reset_customizes": "重設為預設自訂",
|
||||
"bool2selects": "自訂選項"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "連線裝置失敗",
|
||||
"cannot_login": "小米帳號登入失敗,請檢查帳號密碼。如多次嘗試仍登入失敗,請在與目前網路相同的環境下登入小米官網(mi.com)後重試",
|
||||
"cannot_reach": "無法存取小米 API",
|
||||
"tzinfo_error": "您的 Hass 環境看起來有點問題,請參考: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "該小米帳號下沒有裝置"
|
||||
},
|
||||
"abort": {
|
||||
"config_saved": "設定已儲存。\n{tip}\n\n有些自訂選項需要[重新載入整合](/developer-tools/yaml)才能生效。",
|
||||
"already_configured": "該裝置已經設定過",
|
||||
"not_xiaomi_miio": "該裝置不支援 miio/miot 協定"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Xiaomi Miot",
|
||||
"description": "設定裝置資訊",
|
||||
"data": {
|
||||
"host": "裝置 IP",
|
||||
"token": "Token",
|
||||
"scan_interval": "狀態更新頻率(秒)",
|
||||
"miot_cloud": "開啟雲端模式(請先設定帳號密碼)"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
"description": "設定小米帳號\n{tip}",
|
||||
"data": {
|
||||
"username": "小米 ID / 電子郵件",
|
||||
"password": "小米密碼",
|
||||
"captcha": "驗證碼",
|
||||
"server_country": "小米伺服器",
|
||||
"conn_mode": "裝置連線模式",
|
||||
"renew_devices": "更新裝置列表",
|
||||
"trans_options": "翻譯屬性值描述",
|
||||
"disable_message": "停用米家 APP 通知訊息實體",
|
||||
"disable_scene_history": "停用米家場景歷史實體"
|
||||
}
|
||||
},
|
||||
"cloud_filter": {
|
||||
"title": "篩選裝置",
|
||||
"description": "你可以透過篩選只接入想要的裝置。在排除模式中,如果不勾選任何裝置,則相當於接入所有裝置。如果想整合網關子裝置,請直接選擇子裝置,接入網關不會自動整合子裝置。\n{tip}",
|
||||
"data": {
|
||||
"filter_model": "篩選裝置型號",
|
||||
"model_list": "裝置型號列表",
|
||||
"filter_home_id": "篩選家庭",
|
||||
"home_id_list": "家庭列表",
|
||||
"filter_ssid": "篩選 WiFi SSID",
|
||||
"ssid_list": "WiFi SSID 列表",
|
||||
"filter_bssid": "篩選 WiFi BSSID",
|
||||
"bssid_list": "WiFi BSSID 列表",
|
||||
"filter_did": "篩選裝置",
|
||||
"did_list": "裝置列表",
|
||||
"home_ids": "篩選家庭 (留白儲存)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "連線裝置失敗",
|
||||
"cannot_login": "小米帳號登入失敗,請檢查帳號密碼。如多次嘗試仍登入失敗,請使用與 HA 主機同一區域網路下的其他裝置登入小米官網(mi.com)後重試",
|
||||
"cannot_reach": "無法存取小米 API",
|
||||
"tzinfo_error": "您的 Hass 環境看起來有點問題,請參考: https://github.com/al-one/hass-xiaomi-miot/issues/260",
|
||||
"none_devices": "該小米帳號下沒有裝置"
|
||||
},
|
||||
"abort": {
|
||||
"show_customizes": "[](/_my_redirect/config_flow_start?domain=xiaomi_miot)\n請再次添加[`Xiaomi Miot`](/_my_redirect/config_flow_start?domain=xiaomi_miot)整合來修改自訂選項\n{tip}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"component_version": "自訂元件版本",
|
||||
"can_reach_server": "可存取米家伺服器",
|
||||
"can_reach_spec": "可取得 MIoT 規格",
|
||||
"logged_accounts": "已登入的帳號數量",
|
||||
"total_devices": "米家裝置總數量"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"info": {"name": "資訊"}
|
||||
},
|
||||
"sensor": {
|
||||
"clean_area": {"name": "清潔面積"},
|
||||
"clean_time": {"name": "清潔時間"},
|
||||
"power_cost_today": {"name": "日用電量"},
|
||||
"power_cost_month": {"name": "月用電量"},
|
||||
"power_cost_today_2": {"name": "日用電量"},
|
||||
"power_cost_month_2": {"name": "月用電量"},
|
||||
"prop_cal_day-power_cost-today": {"name": "日用電量"},
|
||||
"prop_cal_day-power_cost-month": {"name": "月用電量"},
|
||||
"props-clean_area": {"name": "清潔面積"},
|
||||
"props-clean_time": {"name": "清潔時間"},
|
||||
"lock": {
|
||||
"state": {
|
||||
"bluetooth": "藍牙",
|
||||
"password": "密碼",
|
||||
"biological": "生物辨識",
|
||||
"key": "鑰匙",
|
||||
"turntable": "轉盤",
|
||||
"nfc": "NFC",
|
||||
"one_time_password": "一次性密碼",
|
||||
"two_step_verification": "兩步驟驗證",
|
||||
"coercion": "脅迫",
|
||||
"homekit": "HomeKit",
|
||||
"manual": "手動",
|
||||
"automatic": "自動"
|
||||
}
|
||||
},
|
||||
"lock_action": {
|
||||
"state": {
|
||||
"outside_unlock": "門外解鎖",
|
||||
"lock": "上鎖",
|
||||
"anti_lock_on": "開啟反鎖",
|
||||
"anti_lock_off": "解除反鎖",
|
||||
"inside_unlock": "門內解鎖",
|
||||
"lock_inside": "門內上鎖",
|
||||
"child_lock_on": "開啟兒童鎖",
|
||||
"child_lock_off": "關閉兒童鎖",
|
||||
"lock_outside": "門外上鎖"
|
||||
}
|
||||
},
|
||||
"door_state": {
|
||||
"state": {
|
||||
"open": "開門",
|
||||
"close": "關門",
|
||||
"close_timeout": "超時未關",
|
||||
"knock": "敲門",
|
||||
"breaking": "撬門",
|
||||
"stuck": "卡住"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"auto": "自動",
|
||||
"low": "低速",
|
||||
"medium": "中速",
|
||||
"middle": "中速",
|
||||
"high": "高速",
|
||||
"quiet": "靜音",
|
||||
"turbo": "超強",
|
||||
"level1": "速度1",
|
||||
"level2": "速度2",
|
||||
"level3": "速度3",
|
||||
"level4": "速度4",
|
||||
"level5": "速度5",
|
||||
"level6": "速度6",
|
||||
"level7": "速度7",
|
||||
"level8": "速度8",
|
||||
"leve7": "速度7",
|
||||
"leve8": "速度8"
|
||||
}
|
||||
},
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"off": "關閉",
|
||||
"auto": "自動",
|
||||
"Heat": "暖氣",
|
||||
"Warm": "溫暖",
|
||||
"Natural Wind": "自然風"
|
||||
}
|
||||
},
|
||||
"swing_mode": {
|
||||
"state": {
|
||||
"off": "關閉",
|
||||
"vertical": "垂直",
|
||||
"horizontal": "水平",
|
||||
"both": "立體"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"fan": {
|
||||
"state_attributes": {
|
||||
"preset_modes": {
|
||||
"state": {
|
||||
"Basic Wind": "標準風",
|
||||
"Straight Wind": "直吹風",
|
||||
"Natural Wind": "自然風",
|
||||
"Circular Wind": "循環風",
|
||||
"Baby": "嬰兒",
|
||||
"Sleep": "睡眠",
|
||||
"Smart": "智慧"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"light": {
|
||||
"state_attributes": {
|
||||
"effect_list": {
|
||||
"state": {
|
||||
"Day": "日光",
|
||||
"Night": "夜光",
|
||||
"Color": "彩光",
|
||||
"Scene": "場景",
|
||||
"Music": "音樂"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"fan_control-fan_level": {
|
||||
"state": {
|
||||
"auto": "自動",
|
||||
"low": "低速",
|
||||
"medium": "中速",
|
||||
"middle": "中速",
|
||||
"high": "高速",
|
||||
"Quiet": "靜音",
|
||||
"Turbo": "超強",
|
||||
"Level1": "速度1",
|
||||
"Level2": "速度2",
|
||||
"Level3": "速度3",
|
||||
"Level4": "速度4",
|
||||
"Level5": "速度5",
|
||||
"Level6": "速度6",
|
||||
"Level7": "速度7",
|
||||
"Level8": "速度8",
|
||||
"Leve7": "速度7",
|
||||
"Leve8": "速度8"
|
||||
}
|
||||
},
|
||||
"light-mode": {
|
||||
"Day": "日光",
|
||||
"Night": "夜光",
|
||||
"Color": "彩光",
|
||||
"Scene": "場景",
|
||||
"Music": "音樂"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_command": {
|
||||
"description": "發送 miio 指令",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "實體 ID"
|
||||
},
|
||||
"method": {
|
||||
"description": "Miio 方法"
|
||||
},
|
||||
"params": {
|
||||
"description": "Miio 參數"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_properties": {
|
||||
"description": "取得 MIoT 屬性",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "實體 ID"
|
||||
},
|
||||
"mapping": {
|
||||
"description": "MIoT 規格"
|
||||
},
|
||||
"update_entity": {
|
||||
"description": "更新實體狀態屬性"
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_property": {
|
||||
"description": "設定 MIoT 屬性(透過屬性名)",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "實體 ID"
|
||||
},
|
||||
"field": {
|
||||
"description": "MIoT 屬性名"
|
||||
},
|
||||
"value": {
|
||||
"description": "MIoT 屬性值"
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_miot_property": {
|
||||
"description": "設定 MIoT 屬性(透過 siid/piid)",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "實體 ID"
|
||||
},
|
||||
"siid": {
|
||||
"description": "MIoT 服務 ID"
|
||||
},
|
||||
"piid": {
|
||||
"description": "MIoT 屬性 ID"
|
||||
},
|
||||
"value": {
|
||||
"description": "MIoT 屬性值"
|
||||
}
|
||||
}
|
||||
},
|
||||
"call_action": {
|
||||
"description": "執行 MIoT 動作",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "實體 ID"
|
||||
},
|
||||
"siid": {
|
||||
"description": "MIoT 服務 ID"
|
||||
},
|
||||
"aiid": {
|
||||
"description": "MIoT 動作 ID"
|
||||
},
|
||||
"params": {
|
||||
"description": "MIoT 動作參數"
|
||||
}
|
||||
}
|
||||
},
|
||||
"intelligent_speaker": {
|
||||
"description": "小愛 TTS 及執行語音指令",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "小愛實體 ID"
|
||||
},
|
||||
"text": {
|
||||
"description": "文字內容"
|
||||
},
|
||||
"execute": {
|
||||
"description": "執行語音指令,否則朗讀文字"
|
||||
},
|
||||
"silent": {
|
||||
"description": "靜默執行語音指令"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xiaoai_wakeup": {
|
||||
"description": "喚醒小愛音箱",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "小愛實體 ID"
|
||||
},
|
||||
"text": {
|
||||
"description": "文字內容"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_device_data": {
|
||||
"description": "取得裝置資訊",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "實體 ID"
|
||||
},
|
||||
"type": {
|
||||
"name": "類型",
|
||||
"description": ""
|
||||
},
|
||||
"key": {
|
||||
"name": "金鑰",
|
||||
"description": ""
|
||||
},
|
||||
"time_start": {
|
||||
"name": "開始時間",
|
||||
"description": ""
|
||||
},
|
||||
"time_end": {
|
||||
"name": "結束時間",
|
||||
"description": ""
|
||||
},
|
||||
"limit": {
|
||||
"name": "限制",
|
||||
"description": ""
|
||||
},
|
||||
"group": {
|
||||
"name": "群組",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"renew_devices": {
|
||||
"description": "重新整理裝置列表",
|
||||
"fields": {
|
||||
"username": {
|
||||
"name": "小米帳號",
|
||||
"description": "小米 ID / 手機號碼 / 電子郵件"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_token": {
|
||||
"description": "取得米家裝置令牌(token)",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "關鍵字",
|
||||
"description": "米家 APP 中的裝置名稱關鍵字 / IP / 型號"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_bindkey": {
|
||||
"description": "取得藍牙裝置 bindkey",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "實體 ID"
|
||||
},
|
||||
"did": {
|
||||
"description": "裝置 DID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"request_xiaomi_api": {
|
||||
"description": "呼叫小米雲端 API",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "實體 ID"
|
||||
},
|
||||
"api": {
|
||||
"description": "API 網址"
|
||||
},
|
||||
"data": {
|
||||
"description": "傳送資料"
|
||||
},
|
||||
"method": {
|
||||
"description": "呼叫方式"
|
||||
},
|
||||
"crypt": {
|
||||
"description": "是否加密"
|
||||
},
|
||||
"sid": {
|
||||
"description": "小米服務 ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user