mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2025-03-28 21:35:29 +08:00
feat: support modify spec and value conversion (#663)
* fix: fix miot_device type error * fix: fix type error * feat: remove spec cache storage * feat: update std_lib and multi_lang logic * feat: update entity value-range * feat: update value-list logic * feat: update prop.format_ logic * fix: fix miot cloud log error * fix: fix fan entity * style: ignore type error * style: rename spec_filter func name * feat: move bool_trans from storage to spec * feat: move sepc_filter from storage to spec, use the YAML format file * feat: same prop supports multiple sub * feat: same event supports multiple sub * fix: fix device remove error * feat: add func slugify_did * fix: fix multi lang error * feat: update action debug logic * feat: ignore normal disconnect log * feat: support binary mode * feat: change miot spec name type define * style: ignore i18n tranlate type error * fix: fix pylint warning * fix: miot storage type error * feat: support binary display mode configure * feat: set default sensor state_class * fix: fix sensor entity type error * fix: fix __init__ type error * feat: update test case logic * fix: github actions add dependencies lib * fix: fix some type error * feat: update device list changed notify logic * feat: update prop expr logic * feat: add spec modify * feat: update device sub id logic * feat: update get miot client instance logic * fix: fix some type error * feat: update miot device unit and icon trans * perf: update spec trans entity logic * feat: update spec trans entity rule * feat: update spec_modify * feat: update sensor ENUM icon * fix: fix miot device error * fix: fix miot spec error * featL update format check and spec modify file * feat: update checkout rule format * feat: handle special property.unit * feat: add expr for cuco-cp1md * feat: fix climate hvac error * feat: set sensor suggested display precision * feat: update climate set hvac logic * feat: add expr for cuco-v3 * feat: update spec expr for chuangmi-212a01
This commit is contained in:
parent
3c16f0ffbb
commit
8778b00c3a
@ -258,13 +258,14 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||
f'{self.entity_id}')
|
||||
return
|
||||
# set air-conditioner on
|
||||
elif self.get_prop_value(prop=self._prop_on) is False:
|
||||
await self.set_property_async(prop=self._prop_on, value=True)
|
||||
if self.get_prop_value(prop=self._prop_on) is False:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_on, value=True, update=False)
|
||||
# set mode
|
||||
mode_value = self.get_map_key(
|
||||
map_=self._hvac_mode_map, value=hvac_mode)
|
||||
if (
|
||||
mode_value is None or
|
||||
not mode_value or
|
||||
not await self.set_property_async(
|
||||
prop=self._prop_mode, value=mode_value)
|
||||
):
|
||||
@ -368,9 +369,9 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity):
|
||||
"""Return the hvac mode. e.g., heat, cool mode."""
|
||||
if self.get_prop_value(prop=self._prop_on) is False:
|
||||
return HVACMode.OFF
|
||||
return self.get_map_key(
|
||||
return self.get_map_value(
|
||||
map_=self._hvac_mode_map,
|
||||
value=self.get_prop_value(prop=self._prop_mode))
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> Optional[str]:
|
||||
|
@ -150,7 +150,7 @@ class MIoTClient:
|
||||
# Device list update timestamp
|
||||
_device_list_update_ts: int
|
||||
|
||||
_sub_source_list: dict[str, str]
|
||||
_sub_source_list: dict[str, Optional[str]]
|
||||
_sub_tree: MIoTMatcher
|
||||
_sub_device_state: dict[str, MipsDeviceState]
|
||||
|
||||
@ -620,7 +620,7 @@ class MIoTClient:
|
||||
# Priority local control
|
||||
if self._ctrl_mode == CtrlMode.AUTO:
|
||||
# Gateway control
|
||||
device_gw: dict = self._device_list_gateway.get(did, None)
|
||||
device_gw = self._device_list_gateway.get(did, None)
|
||||
if (
|
||||
device_gw and device_gw.get('online', False)
|
||||
and device_gw.get('specv2_access', False)
|
||||
@ -641,7 +641,7 @@ class MIoTClient:
|
||||
raise MIoTClientError(
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# Lan control
|
||||
device_lan: dict = self._device_list_lan.get(did, None)
|
||||
device_lan = self._device_list_lan.get(did, None)
|
||||
if device_lan and device_lan.get('online', False):
|
||||
result = await self._miot_lan.set_prop_async(
|
||||
did=did, siid=siid, piid=piid, value=value)
|
||||
@ -657,7 +657,7 @@ class MIoTClient:
|
||||
# Cloud control
|
||||
device_cloud = self._device_list_cloud.get(did, None)
|
||||
if device_cloud and device_cloud.get('online', False):
|
||||
result: list = await self._http.set_prop_async(
|
||||
result = await self._http.set_prop_async(
|
||||
params=[
|
||||
{'did': did, 'siid': siid, 'piid': piid, 'value': value}
|
||||
])
|
||||
@ -746,7 +746,7 @@ class MIoTClient:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
|
||||
device_gw: dict = self._device_list_gateway.get(did, None)
|
||||
device_gw = self._device_list_gateway.get(did, None)
|
||||
# Priority local control
|
||||
if self._ctrl_mode == CtrlMode.AUTO:
|
||||
if (
|
||||
@ -782,7 +782,7 @@ class MIoTClient:
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# Cloud control
|
||||
device_cloud = self._device_list_cloud.get(did, None)
|
||||
if device_cloud.get('online', False):
|
||||
if device_cloud and device_cloud.get('online', False):
|
||||
result: dict = await self._http.action_async(
|
||||
did=did, siid=siid, aiid=aiid, in_list=in_list)
|
||||
if result:
|
||||
@ -798,14 +798,15 @@ class MIoTClient:
|
||||
dids=[did]))
|
||||
raise MIoTClientError(
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# Show error message
|
||||
# TODO: Show error message
|
||||
_LOGGER.error(
|
||||
'client action failed, %s.%d.%d', did, siid, aiid)
|
||||
return None
|
||||
return []
|
||||
|
||||
def sub_prop(
|
||||
self, did: str, handler: Callable[[dict, Any], None],
|
||||
siid: int = None, piid: int = None, handler_ctx: Any = None
|
||||
siid: Optional[int] = None, piid: Optional[int] = None,
|
||||
handler_ctx: Any = None
|
||||
) -> bool:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
@ -818,7 +819,9 @@ class MIoTClient:
|
||||
_LOGGER.debug('client sub prop, %s', topic)
|
||||
return True
|
||||
|
||||
def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool:
|
||||
def unsub_prop(
|
||||
self, did: str, siid: Optional[int] = None, piid: Optional[int] = None
|
||||
) -> bool:
|
||||
topic = (
|
||||
f'{did}/p/'
|
||||
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
|
||||
@ -829,7 +832,8 @@ class MIoTClient:
|
||||
|
||||
def sub_event(
|
||||
self, did: str, handler: Callable[[dict, Any], None],
|
||||
siid: int = None, eiid: int = None, handler_ctx: Any = None
|
||||
siid: Optional[int] = None, eiid: Optional[int] = None,
|
||||
handler_ctx: Any = None
|
||||
) -> bool:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
@ -841,7 +845,9 @@ class MIoTClient:
|
||||
_LOGGER.debug('client sub event, %s', topic)
|
||||
return True
|
||||
|
||||
def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool:
|
||||
def unsub_event(
|
||||
self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None
|
||||
) -> bool:
|
||||
topic = (
|
||||
f'{did}/e/'
|
||||
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
|
||||
@ -1081,7 +1087,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self.__request_show_devices_changed_notify()
|
||||
@ -1091,8 +1097,8 @@ class MIoTClient:
|
||||
self, group_id: str, state: bool
|
||||
) -> None:
|
||||
_LOGGER.info('local mips state changed, %s, %s', group_id, state)
|
||||
mips: MipsLocalClient = self._mips_local.get(group_id, None)
|
||||
if mips is None:
|
||||
mips = self._mips_local.get(group_id, None)
|
||||
if not mips:
|
||||
_LOGGER.error(
|
||||
'local mips state changed, mips not exist, %s', group_id)
|
||||
return
|
||||
@ -1124,7 +1130,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self.__request_show_devices_changed_notify()
|
||||
@ -1171,7 +1177,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self._device_list_lan = {}
|
||||
@ -1201,7 +1207,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
return
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1257,7 +1263,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
return
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1301,9 +1307,8 @@ class MIoTClient:
|
||||
async def __load_cache_device_async(self) -> None:
|
||||
"""Load device list from cache."""
|
||||
cache_list: Optional[dict[str, dict]] = await self._storage.load_async(
|
||||
domain='miot_devices',
|
||||
name=f'{self._uid}_{self._cloud_server}',
|
||||
type_=dict)
|
||||
domain='miot_devices', name=f'{self._uid}_{self._cloud_server}',
|
||||
type_=dict) # type: ignore
|
||||
if not cache_list:
|
||||
self.__show_client_error_notify(
|
||||
message=self._i18n.translate(
|
||||
@ -1346,7 +1351,7 @@ class MIoTClient:
|
||||
cloud_state_old: Optional[bool] = self._device_list_cloud.get(
|
||||
did, {}).get('online', None)
|
||||
cloud_state_new: Optional[bool] = None
|
||||
device_new: dict = cloud_list.pop(did, None)
|
||||
device_new = cloud_list.pop(did, None)
|
||||
if device_new:
|
||||
cloud_state_new = device_new.get('online', None)
|
||||
# Update cache device info
|
||||
@ -1371,7 +1376,7 @@ class MIoTClient:
|
||||
continue
|
||||
info['online'] = state_new
|
||||
# Call device state changed callback
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1426,8 +1431,7 @@ class MIoTClient:
|
||||
self, dids: list[str]
|
||||
) -> None:
|
||||
_LOGGER.debug('refresh cloud device with dids, %s', dids)
|
||||
cloud_list: dict[str, dict] = (
|
||||
await self._http.get_devices_with_dids_async(dids=dids))
|
||||
cloud_list = await self._http.get_devices_with_dids_async(dids=dids)
|
||||
if cloud_list is None:
|
||||
_LOGGER.error('cloud http get_dev_list_async failed, %s', dids)
|
||||
return
|
||||
@ -1466,11 +1470,11 @@ class MIoTClient:
|
||||
for did, info in self._device_list_cache.items():
|
||||
if did not in filter_dids:
|
||||
continue
|
||||
device_old: dict = self._device_list_gateway.get(did, None)
|
||||
device_old = self._device_list_gateway.get(did, None)
|
||||
gw_state_old = device_old.get(
|
||||
'online', False) if device_old else False
|
||||
gw_state_new: bool = False
|
||||
device_new: dict = gw_list.pop(did, None)
|
||||
device_new = gw_list.pop(did, None)
|
||||
if device_new:
|
||||
# Update gateway device info
|
||||
self._device_list_gateway[did] = {
|
||||
@ -1493,7 +1497,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
info['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1518,7 +1522,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1533,7 +1537,7 @@ class MIoTClient:
|
||||
'refresh gw devices with group_id, %s', group_id)
|
||||
# Remove timer
|
||||
self._mips_local_state_changed_timers.pop(group_id, None)
|
||||
mips: MipsLocalClient = self._mips_local.get(group_id, None)
|
||||
mips = self._mips_local.get(group_id, None)
|
||||
if not mips:
|
||||
_LOGGER.error('mips not exist, %s', group_id)
|
||||
return
|
||||
@ -1900,77 +1904,73 @@ async def get_miot_instance_async(
|
||||
) -> MIoTClient:
|
||||
if entry_id is None:
|
||||
raise MIoTClientError('invalid entry_id')
|
||||
miot_client: MIoTClient = None
|
||||
if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None):
|
||||
miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None)
|
||||
if miot_client:
|
||||
_LOGGER.info('instance exist, %s', entry_id)
|
||||
miot_client = a
|
||||
else:
|
||||
if entry_data is None:
|
||||
raise MIoTClientError('entry data is None')
|
||||
# Get running loop
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||
if loop is None:
|
||||
raise MIoTClientError('loop is None')
|
||||
# MIoT storage
|
||||
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
|
||||
'miot_storage', None)
|
||||
if not storage:
|
||||
storage = MIoTStorage(
|
||||
root_path=entry_data['storage_path'], loop=loop)
|
||||
hass.data[DOMAIN]['miot_storage'] = storage
|
||||
_LOGGER.info('create miot_storage instance')
|
||||
global_config: dict = await storage.load_user_config_async(
|
||||
uid='global_config', cloud_server='all',
|
||||
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
|
||||
# MIoT network
|
||||
network_detect_addr: dict = global_config.get(
|
||||
'network_detect_addr', {})
|
||||
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
|
||||
'miot_network', None)
|
||||
if not network:
|
||||
network = MIoTNetwork(
|
||||
ip_addr_list=network_detect_addr.get('ip', []),
|
||||
url_addr_list=network_detect_addr.get('url', []),
|
||||
refresh_interval=NETWORK_REFRESH_INTERVAL,
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_network'] = network
|
||||
await network.init_async()
|
||||
_LOGGER.info('create miot_network instance')
|
||||
# MIoT service
|
||||
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
|
||||
'mips_service', None)
|
||||
if not mips_service:
|
||||
aiozc = await zeroconf.async_get_async_instance(hass)
|
||||
mips_service = MipsService(aiozc=aiozc, loop=loop)
|
||||
hass.data[DOMAIN]['mips_service'] = mips_service
|
||||
await mips_service.init_async()
|
||||
_LOGGER.info('create mips_service instance')
|
||||
# MIoT lan
|
||||
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get(
|
||||
'miot_lan', None)
|
||||
if not miot_lan:
|
||||
miot_lan = MIoTLan(
|
||||
net_ifs=global_config.get('net_interfaces', []),
|
||||
network=network,
|
||||
mips_service=mips_service,
|
||||
enable_subscribe=global_config.get('enable_subscribe', False),
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_lan'] = miot_lan
|
||||
_LOGGER.info('create miot_lan instance')
|
||||
# MIoT client
|
||||
miot_client = MIoTClient(
|
||||
entry_id=entry_id,
|
||||
entry_data=entry_data,
|
||||
return miot_client
|
||||
# Create new instance
|
||||
if not entry_data:
|
||||
raise MIoTClientError('entry data is None')
|
||||
# Get running loop
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||
if not loop:
|
||||
raise MIoTClientError('loop is None')
|
||||
# MIoT storage
|
||||
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
|
||||
'miot_storage', None)
|
||||
if not storage:
|
||||
storage = MIoTStorage(
|
||||
root_path=entry_data['storage_path'], loop=loop)
|
||||
hass.data[DOMAIN]['miot_storage'] = storage
|
||||
_LOGGER.info('create miot_storage instance')
|
||||
global_config: dict = await storage.load_user_config_async(
|
||||
uid='global_config', cloud_server='all',
|
||||
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
|
||||
# MIoT network
|
||||
network_detect_addr: dict = global_config.get('network_detect_addr', {})
|
||||
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
|
||||
'miot_network', None)
|
||||
if not network:
|
||||
network = MIoTNetwork(
|
||||
ip_addr_list=network_detect_addr.get('ip', []),
|
||||
url_addr_list=network_detect_addr.get('url', []),
|
||||
refresh_interval=NETWORK_REFRESH_INTERVAL,
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_network'] = network
|
||||
await network.init_async()
|
||||
_LOGGER.info('create miot_network instance')
|
||||
# MIoT service
|
||||
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
|
||||
'mips_service', None)
|
||||
if not mips_service:
|
||||
aiozc = await zeroconf.async_get_async_instance(hass)
|
||||
mips_service = MipsService(aiozc=aiozc, loop=loop)
|
||||
hass.data[DOMAIN]['mips_service'] = mips_service
|
||||
await mips_service.init_async()
|
||||
_LOGGER.info('create mips_service instance')
|
||||
# MIoT lan
|
||||
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None)
|
||||
if not miot_lan:
|
||||
miot_lan = MIoTLan(
|
||||
net_ifs=global_config.get('net_interfaces', []),
|
||||
network=network,
|
||||
storage=storage,
|
||||
mips_service=mips_service,
|
||||
miot_lan=miot_lan,
|
||||
loop=loop
|
||||
)
|
||||
miot_client.persistent_notify = persistent_notify
|
||||
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
|
||||
_LOGGER.info(
|
||||
'new miot_client instance, %s, %s', entry_id, entry_data)
|
||||
await miot_client.init_async()
|
||||
|
||||
enable_subscribe=global_config.get('enable_subscribe', False),
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_lan'] = miot_lan
|
||||
_LOGGER.info('create miot_lan instance')
|
||||
# MIoT client
|
||||
miot_client = MIoTClient(
|
||||
entry_id=entry_id,
|
||||
entry_data=entry_data,
|
||||
network=network,
|
||||
storage=storage,
|
||||
mips_service=mips_service,
|
||||
miot_lan=miot_lan,
|
||||
loop=loop
|
||||
)
|
||||
miot_client.persistent_notify = persistent_notify
|
||||
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
|
||||
_LOGGER.info('new miot_client instance, %s, %s', entry_id, entry_data)
|
||||
await miot_client.init_async()
|
||||
return miot_client
|
||||
|
@ -382,7 +382,7 @@ class MIoTHttpClient:
|
||||
|
||||
return res_obj['data']
|
||||
|
||||
async def get_central_cert_async(self, csr: str) -> Optional[str]:
|
||||
async def get_central_cert_async(self, csr: str) -> str:
|
||||
if not isinstance(csr, str):
|
||||
raise MIoTHttpError('invalid params')
|
||||
|
||||
|
@ -56,6 +56,7 @@ from homeassistant.const import (
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
DEGREE,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
@ -72,6 +73,7 @@ from homeassistant.const import (
|
||||
UnitOfPower,
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
UnitOfDataRate
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
@ -243,12 +245,12 @@ class MIoTDevice:
|
||||
def sub_device_state(
|
||||
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
|
||||
) -> int:
|
||||
self._sub_id += 1
|
||||
sub_id = self.__gen_sub_id()
|
||||
if key in self._device_state_sub_list:
|
||||
self._device_state_sub_list[key][str(self._sub_id)] = handler
|
||||
self._device_state_sub_list[key][str(sub_id)] = handler
|
||||
else:
|
||||
self._device_state_sub_list[key] = {str(self._sub_id): handler}
|
||||
return self._sub_id
|
||||
self._device_state_sub_list[key] = {str(sub_id): handler}
|
||||
return sub_id
|
||||
|
||||
def unsub_device_state(self, key: str, sub_id: int) -> None:
|
||||
sub_list = self._device_state_sub_list.get(key, None)
|
||||
@ -266,14 +268,14 @@ class MIoTDevice:
|
||||
for handler in self._value_sub_list[key].values():
|
||||
handler(params, ctx)
|
||||
|
||||
self._sub_id += 1
|
||||
sub_id = self.__gen_sub_id()
|
||||
if key in self._value_sub_list:
|
||||
self._value_sub_list[key][str(self._sub_id)] = handler
|
||||
self._value_sub_list[key][str(sub_id)] = handler
|
||||
else:
|
||||
self._value_sub_list[key] = {str(self._sub_id): handler}
|
||||
self._value_sub_list[key] = {str(sub_id): handler}
|
||||
self.miot_client.sub_prop(
|
||||
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
|
||||
return self._sub_id
|
||||
return sub_id
|
||||
|
||||
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
|
||||
key: str = f'p.{siid}.{piid}'
|
||||
@ -294,14 +296,14 @@ class MIoTDevice:
|
||||
for handler in self._value_sub_list[key].values():
|
||||
handler(params, ctx)
|
||||
|
||||
self._sub_id += 1
|
||||
sub_id = self.__gen_sub_id()
|
||||
if key in self._value_sub_list:
|
||||
self._value_sub_list[key][str(self._sub_id)] = handler
|
||||
self._value_sub_list[key][str(sub_id)] = handler
|
||||
else:
|
||||
self._value_sub_list[key] = {str(self._sub_id): handler}
|
||||
self._value_sub_list[key] = {str(sub_id): handler}
|
||||
self.miot_client.sub_event(
|
||||
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
|
||||
return self._sub_id
|
||||
return sub_id
|
||||
|
||||
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
|
||||
key: str = f'e.{siid}.{eiid}'
|
||||
@ -414,10 +416,12 @@ class MIoTDevice:
|
||||
spec_name: str = spec_instance.name
|
||||
if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str):
|
||||
spec_name = SPEC_DEVICE_TRANS_MAP[spec_name]
|
||||
if 'required' not in SPEC_DEVICE_TRANS_MAP[spec_name]:
|
||||
return None
|
||||
# 1. The device shall have all required services.
|
||||
required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys()
|
||||
if not {
|
||||
service.name for service in spec_instance.services
|
||||
service.name for service in spec_instance.services
|
||||
}.issuperset(required_services):
|
||||
return None
|
||||
optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys()
|
||||
@ -427,9 +431,13 @@ class MIoTDevice:
|
||||
for service in spec_instance.services:
|
||||
if service.platform:
|
||||
continue
|
||||
required_properties: dict
|
||||
optional_properties: dict
|
||||
required_actions: set
|
||||
optional_actions: set
|
||||
# 2. The service shall have all required properties, actions.
|
||||
if service.name in required_services:
|
||||
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
'required'].get(
|
||||
service.name, {}
|
||||
).get('required', {}).get('properties', {})
|
||||
@ -446,7 +454,7 @@ class MIoTDevice:
|
||||
service.name, {}
|
||||
).get('optional', {}).get('actions', set({}))
|
||||
elif service.name in optional_services:
|
||||
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
'optional'].get(
|
||||
service.name, {}
|
||||
).get('required', {}).get('properties', {})
|
||||
@ -484,7 +492,7 @@ class MIoTDevice:
|
||||
set(required_properties.keys()), optional_properties):
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
# prop.icon = self.icon_convert(prop.unit)
|
||||
prop.platform = platform
|
||||
entity_data.props.add(prop)
|
||||
# action
|
||||
@ -499,85 +507,95 @@ class MIoTDevice:
|
||||
return entity_data
|
||||
|
||||
def parse_miot_service_entity(
|
||||
self, service_instance: MIoTSpecService
|
||||
self, miot_service: MIoTSpecService
|
||||
) -> Optional[MIoTEntityData]:
|
||||
service = service_instance
|
||||
if service.platform or (service.name not in SPEC_SERVICE_TRANS_MAP):
|
||||
if (
|
||||
miot_service.platform
|
||||
or miot_service.name not in SPEC_SERVICE_TRANS_MAP
|
||||
):
|
||||
return None
|
||||
|
||||
service_name = service.name
|
||||
service_name = miot_service.name
|
||||
if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str):
|
||||
service_name = SPEC_SERVICE_TRANS_MAP[service_name]
|
||||
# 1. The service shall have all required properties.
|
||||
if 'required' not in SPEC_SERVICE_TRANS_MAP[service_name]:
|
||||
return None
|
||||
# Required properties, required access mode
|
||||
required_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][
|
||||
'required'].get('properties', {})
|
||||
if not {
|
||||
prop.name for prop in service.properties if prop.access
|
||||
prop.name for prop in miot_service.properties if prop.access
|
||||
}.issuperset(set(required_properties.keys())):
|
||||
return None
|
||||
# 2. The required property shall have all required access mode.
|
||||
for prop in service.properties:
|
||||
for prop in miot_service.properties:
|
||||
if prop.name in required_properties:
|
||||
if not set(prop.access).issuperset(
|
||||
required_properties[prop.name]):
|
||||
return None
|
||||
# Required actions
|
||||
# Required events
|
||||
platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity']
|
||||
entity_data = MIoTEntityData(platform=platform, spec=service_instance)
|
||||
entity_data = MIoTEntityData(platform=platform, spec=miot_service)
|
||||
# Optional properties
|
||||
optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][
|
||||
'optional'].get('properties', set({}))
|
||||
for prop in service.properties:
|
||||
for prop in miot_service.properties:
|
||||
if prop.name in set.union(
|
||||
set(required_properties.keys()), optional_properties):
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
# prop.icon = self.icon_convert(prop.unit)
|
||||
prop.platform = platform
|
||||
entity_data.props.add(prop)
|
||||
# action
|
||||
# event
|
||||
# No actions or events is in SPEC_SERVICE_TRANS_MAP now.
|
||||
service.platform = platform
|
||||
# Optional actions
|
||||
# Optional events
|
||||
miot_service.platform = platform
|
||||
return entity_data
|
||||
|
||||
def parse_miot_property_entity(
|
||||
self, property_instance: MIoTSpecProperty
|
||||
) -> Optional[dict[str, str]]:
|
||||
prop = property_instance
|
||||
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
|
||||
if (
|
||||
prop.platform
|
||||
or (prop.name not in SPEC_PROP_TRANS_MAP['properties'])
|
||||
miot_prop.platform
|
||||
or miot_prop.name not in SPEC_PROP_TRANS_MAP['properties']
|
||||
):
|
||||
return None
|
||||
|
||||
prop_name = prop.name
|
||||
return False
|
||||
prop_name = miot_prop.name
|
||||
if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str):
|
||||
prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name]
|
||||
platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity']
|
||||
# Check
|
||||
prop_access: set = set({})
|
||||
if prop.readable:
|
||||
if miot_prop.readable:
|
||||
prop_access.add('read')
|
||||
if prop.writable:
|
||||
if miot_prop.writable:
|
||||
prop_access.add('write')
|
||||
if prop_access != (SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['access']):
|
||||
return None
|
||||
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||
return False
|
||||
if miot_prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['format']:
|
||||
return None
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||
return False
|
||||
miot_prop.device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||
'device_class']
|
||||
result = {'platform': platform, 'device_class': device_class}
|
||||
# optional:
|
||||
if 'optional' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
|
||||
optional = SPEC_PROP_TRANS_MAP['properties'][prop_name]['optional']
|
||||
if 'state_class' in optional:
|
||||
result['state_class'] = optional['state_class']
|
||||
if not prop.unit and 'unit_of_measurement' in optional:
|
||||
result['unit_of_measurement'] = optional['unit_of_measurement']
|
||||
return result
|
||||
# Optional params
|
||||
if 'state_class' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
|
||||
miot_prop.state_class = SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]['state_class']
|
||||
if (
|
||||
not miot_prop.external_unit
|
||||
and 'unit_of_measurement' in SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]
|
||||
):
|
||||
# Priority: spec_modify.unit > unit_convert > specv2entity.unit
|
||||
miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]['unit_of_measurement']
|
||||
if (
|
||||
not miot_prop.icon
|
||||
and 'icon' in SPEC_PROP_TRANS_MAP['properties'][prop_name]
|
||||
):
|
||||
# Priority: spec_modify.icon > icon_convert > specv2entity.icon
|
||||
miot_prop.icon = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||
'icon']
|
||||
miot_prop.platform = platform
|
||||
return True
|
||||
|
||||
def spec_transform(self) -> None:
|
||||
"""Parse service, property, event, action from device spec."""
|
||||
@ -589,7 +607,7 @@ class MIoTDevice:
|
||||
# STEP 2: service conversion
|
||||
for service in self.spec_instance.services:
|
||||
service_entity = self.parse_miot_service_entity(
|
||||
service_instance=service)
|
||||
miot_service=service)
|
||||
if service_entity:
|
||||
self.append_entity(entity_data=service_entity)
|
||||
# STEP 3.1: property conversion
|
||||
@ -598,20 +616,11 @@ class MIoTDevice:
|
||||
continue
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
prop_entity = self.parse_miot_property_entity(
|
||||
property_instance=prop)
|
||||
if prop_entity:
|
||||
prop.platform = prop_entity['platform']
|
||||
prop.device_class = prop_entity['device_class']
|
||||
if 'state_class' in prop_entity:
|
||||
prop.state_class = prop_entity['state_class']
|
||||
if 'unit_of_measurement' in prop_entity:
|
||||
prop.external_unit = self.unit_convert(
|
||||
prop_entity['unit_of_measurement'])
|
||||
prop.icon = self.icon_convert(
|
||||
prop_entity['unit_of_measurement'])
|
||||
# general conversion
|
||||
if not prop.icon:
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
# Special conversion
|
||||
self.parse_miot_property_entity(miot_prop=prop)
|
||||
# General conversion
|
||||
if not prop.platform:
|
||||
if prop.writable:
|
||||
if prop.format_ == str:
|
||||
@ -625,7 +634,7 @@ class MIoTDevice:
|
||||
prop.platform = 'number'
|
||||
else:
|
||||
# Irregular property will not be transformed.
|
||||
pass
|
||||
continue
|
||||
elif prop.readable or prop.notifiable:
|
||||
if prop.format_ == bool:
|
||||
prop.platform = 'binary_sensor'
|
||||
@ -653,11 +662,66 @@ class MIoTDevice:
|
||||
self.append_action(action=action)
|
||||
|
||||
def unit_convert(self, spec_unit: str) -> Optional[str]:
|
||||
"""Convert MIoT unit to Home Assistant unit."""
|
||||
"""Convert MIoT unit to Home Assistant unit.
|
||||
25/01/20: All online prop unit statistical tables: unit, quantity.
|
||||
{
|
||||
"no_unit": 148499,
|
||||
"percentage": 10042,
|
||||
"kelvin": 1895,
|
||||
"rgb": 772, // color
|
||||
"celsius": 5762,
|
||||
"none": 16106,
|
||||
"hours": 1540,
|
||||
"minutes": 5061,
|
||||
"ms": 27,
|
||||
"watt": 216,
|
||||
"arcdegrees": 159,
|
||||
"ppm": 177,
|
||||
"μg/m3": 106,
|
||||
"days": 571,
|
||||
"seconds": 2749,
|
||||
"B/s": 21,
|
||||
"pascal": 110,
|
||||
"mg/m3": 339,
|
||||
"lux": 125,
|
||||
"kWh": 124,
|
||||
"mv": 2,
|
||||
"V": 38,
|
||||
"A": 29,
|
||||
"mV": 4,
|
||||
"L": 352,
|
||||
"m": 37,
|
||||
"毫摩尔每升": 2, // blood-sugar, cholesterol
|
||||
"mmol/L": 1, // urea
|
||||
"weeks": 26,
|
||||
"meter": 3,
|
||||
"dB": 26,
|
||||
"hour": 14,
|
||||
"calorie": 19, // 1 cal = 4.184 J
|
||||
"ppb": 3,
|
||||
"arcdegress": 30,
|
||||
"bpm": 4, // realtime-heartrate
|
||||
"gram": 7,
|
||||
"km/h": 9,
|
||||
"W": 1,
|
||||
"m3/h": 2,
|
||||
"kilopascal": 1,
|
||||
"mL": 4,
|
||||
"mmHg": 4,
|
||||
"w": 1,
|
||||
"liter": 1,
|
||||
"cm": 3,
|
||||
"mA": 2,
|
||||
"kilogram": 2,
|
||||
"kcal/d": 2, // basal-metabolism
|
||||
"times": 1 // exercise-count
|
||||
}
|
||||
"""
|
||||
unit_map = {
|
||||
'percentage': PERCENTAGE,
|
||||
'weeks': UnitOfTime.WEEKS,
|
||||
'days': UnitOfTime.DAYS,
|
||||
'hour': UnitOfTime.HOURS,
|
||||
'hours': UnitOfTime.HOURS,
|
||||
'minutes': UnitOfTime.MINUTES,
|
||||
'seconds': UnitOfTime.SECONDS,
|
||||
@ -672,30 +736,48 @@ class MIoTDevice:
|
||||
'ppb': CONCENTRATION_PARTS_PER_BILLION,
|
||||
'lux': LIGHT_LUX,
|
||||
'pascal': UnitOfPressure.PA,
|
||||
'kilopascal': UnitOfPressure.KPA,
|
||||
'mmHg': UnitOfPressure.MMHG,
|
||||
'bar': UnitOfPressure.BAR,
|
||||
'watt': UnitOfPower.WATT,
|
||||
'L': UnitOfVolume.LITERS,
|
||||
'liter': UnitOfVolume.LITERS,
|
||||
'mL': UnitOfVolume.MILLILITERS,
|
||||
'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
'm/s': UnitOfSpeed.METERS_PER_SECOND,
|
||||
'watt': UnitOfPower.WATT,
|
||||
'w': UnitOfPower.WATT,
|
||||
'W': UnitOfPower.WATT,
|
||||
'kWh': UnitOfEnergy.KILO_WATT_HOUR,
|
||||
'A': UnitOfElectricCurrent.AMPERE,
|
||||
'mA': UnitOfElectricCurrent.MILLIAMPERE,
|
||||
'V': UnitOfElectricPotential.VOLT,
|
||||
'mv': UnitOfElectricPotential.MILLIVOLT,
|
||||
'mV': UnitOfElectricPotential.MILLIVOLT,
|
||||
'cm': UnitOfLength.CENTIMETERS,
|
||||
'm': UnitOfLength.METERS,
|
||||
'meter': UnitOfLength.METERS,
|
||||
'km': UnitOfLength.KILOMETERS,
|
||||
'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
'gram': UnitOfMass.GRAMS,
|
||||
'kilogram': UnitOfMass.KILOGRAMS,
|
||||
'dB': SIGNAL_STRENGTH_DECIBELS,
|
||||
'arcdegrees': DEGREE,
|
||||
'arcdegress': DEGREE,
|
||||
'kB': UnitOfInformation.KILOBYTES,
|
||||
'MB': UnitOfInformation.MEGABYTES,
|
||||
'GB': UnitOfInformation.GIGABYTES,
|
||||
'TB': UnitOfInformation.TERABYTES,
|
||||
'B/s': UnitOfDataRate.BYTES_PER_SECOND,
|
||||
'KB/s': UnitOfDataRate.KILOBYTES_PER_SECOND,
|
||||
'MB/s': UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
'GB/s': UnitOfDataRate.GIGABYTES_PER_SECOND
|
||||
}
|
||||
|
||||
# Handle UnitOfConductivity separately since
|
||||
# it might not be available in all HA versions
|
||||
try:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from homeassistant.const import UnitOfConductivity
|
||||
from homeassistant.const import UnitOfConductivity # type: ignore
|
||||
unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
|
||||
except Exception: # pylint: disable=broad-except
|
||||
unit_map['μS/cm'] = 'μS/cm'
|
||||
@ -703,59 +785,66 @@ class MIoTDevice:
|
||||
return unit_map.get(spec_unit, None)
|
||||
|
||||
def icon_convert(self, spec_unit: str) -> Optional[str]:
|
||||
if spec_unit in ['percentage']:
|
||||
if spec_unit in {'percentage'}:
|
||||
return 'mdi:percent'
|
||||
if spec_unit in [
|
||||
'weeks', 'days', 'hours', 'minutes', 'seconds', 'ms', 'μs']:
|
||||
if spec_unit in {
|
||||
'weeks', 'days', 'hour', 'hours', 'minutes', 'seconds', 'ms', 'μs'
|
||||
}:
|
||||
return 'mdi:clock'
|
||||
if spec_unit in ['celsius']:
|
||||
if spec_unit in {'celsius'}:
|
||||
return 'mdi:temperature-celsius'
|
||||
if spec_unit in ['fahrenheit']:
|
||||
if spec_unit in {'fahrenheit'}:
|
||||
return 'mdi:temperature-fahrenheit'
|
||||
if spec_unit in ['kelvin']:
|
||||
if spec_unit in {'kelvin'}:
|
||||
return 'mdi:temperature-kelvin'
|
||||
if spec_unit in ['μg/m3', 'mg/m3', 'ppm', 'ppb']:
|
||||
if spec_unit in {'μg/m3', 'mg/m3', 'ppm', 'ppb'}:
|
||||
return 'mdi:blur'
|
||||
if spec_unit in ['lux']:
|
||||
if spec_unit in {'lux'}:
|
||||
return 'mdi:brightness-6'
|
||||
if spec_unit in ['pascal', 'megapascal', 'bar']:
|
||||
if spec_unit in {'pascal', 'kilopascal', 'megapascal', 'mmHg', 'bar'}:
|
||||
return 'mdi:gauge'
|
||||
if spec_unit in ['watt']:
|
||||
if spec_unit in {'watt', 'w', 'W'}:
|
||||
return 'mdi:flash-triangle'
|
||||
if spec_unit in ['L', 'mL']:
|
||||
if spec_unit in {'L', 'mL'}:
|
||||
return 'mdi:gas-cylinder'
|
||||
if spec_unit in ['km/h', 'm/s']:
|
||||
if spec_unit in {'km/h', 'm/s'}:
|
||||
return 'mdi:speedometer'
|
||||
if spec_unit in ['kWh']:
|
||||
if spec_unit in {'kWh'}:
|
||||
return 'mdi:transmission-tower'
|
||||
if spec_unit in ['A', 'mA']:
|
||||
if spec_unit in {'A', 'mA'}:
|
||||
return 'mdi:current-ac'
|
||||
if spec_unit in ['V', 'mV']:
|
||||
if spec_unit in {'V', 'mv', 'mV'}:
|
||||
return 'mdi:current-dc'
|
||||
if spec_unit in ['m', 'km']:
|
||||
if spec_unit in {'cm', 'm', 'meter', 'km'}:
|
||||
return 'mdi:ruler'
|
||||
if spec_unit in ['rgb']:
|
||||
if spec_unit in {'rgb'}:
|
||||
return 'mdi:palette'
|
||||
if spec_unit in ['m3/h', 'L/s']:
|
||||
if spec_unit in {'m3/h', 'L/s'}:
|
||||
return 'mdi:pipe-leak'
|
||||
if spec_unit in ['μS/cm']:
|
||||
if spec_unit in {'μS/cm'}:
|
||||
return 'mdi:resistor-nodes'
|
||||
if spec_unit in ['gram']:
|
||||
if spec_unit in {'gram', 'kilogram'}:
|
||||
return 'mdi:weight'
|
||||
if spec_unit in ['dB']:
|
||||
if spec_unit in {'dB'}:
|
||||
return 'mdi:signal-distance-variant'
|
||||
if spec_unit in ['times']:
|
||||
if spec_unit in {'times'}:
|
||||
return 'mdi:counter'
|
||||
if spec_unit in ['mmol/L']:
|
||||
if spec_unit in {'mmol/L'}:
|
||||
return 'mdi:dots-hexagon'
|
||||
if spec_unit in ['arcdegress']:
|
||||
return 'mdi:angle-obtuse'
|
||||
if spec_unit in ['kB']:
|
||||
if spec_unit in {'kB', 'MB', 'GB'}:
|
||||
return 'mdi:network-pos'
|
||||
if spec_unit in ['calorie', 'kCal']:
|
||||
if spec_unit in {'arcdegress', 'arcdegrees'}:
|
||||
return 'mdi:angle-obtuse'
|
||||
if spec_unit in {'B/s', 'KB/s', 'MB/s', 'GB/s'}:
|
||||
return 'mdi:network'
|
||||
if spec_unit in {'calorie', 'kCal'}:
|
||||
return 'mdi:food'
|
||||
return None
|
||||
|
||||
def __gen_sub_id(self) -> int:
|
||||
self._sub_id += 1
|
||||
return self._sub_id
|
||||
|
||||
def __on_device_state_changed(
|
||||
self, did: str, state: MIoTDeviceState, ctx: Any
|
||||
) -> None:
|
||||
@ -1184,6 +1273,7 @@ class MIoTPropertyEntity(Entity):
|
||||
def __on_value_changed(self, params: dict, ctx: Any) -> None:
|
||||
_LOGGER.debug('property changed, %s', params)
|
||||
self._value = self.spec.value_format(params['value'])
|
||||
self._value = self.spec.eval_expr(self._value)
|
||||
if not self._pending_write_ha_state_timer:
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@ -1414,7 +1414,7 @@ class MipsLocalClient(_MipsClient):
|
||||
@final
|
||||
@on_dev_list_changed.setter
|
||||
def on_dev_list_changed(
|
||||
self, func: Callable[[Any, list[str]], Coroutine]
|
||||
self, func: Optional[Callable[[Any, list[str]], Coroutine]]
|
||||
) -> None:
|
||||
"""run in main loop."""
|
||||
self._on_dev_list_changed = func
|
||||
|
@ -469,14 +469,13 @@ class _MIoTSpecBase:
|
||||
proprietary: bool
|
||||
need_filter: bool
|
||||
name: str
|
||||
icon: Optional[str]
|
||||
|
||||
# External params
|
||||
platform: Optional[str]
|
||||
device_class: Any
|
||||
state_class: Any
|
||||
icon: Optional[str]
|
||||
external_unit: Any
|
||||
expression: Optional[str]
|
||||
|
||||
spec_id: int
|
||||
|
||||
@ -489,13 +488,12 @@ class _MIoTSpecBase:
|
||||
self.proprietary = spec.get('proprietary', False)
|
||||
self.need_filter = spec.get('need_filter', False)
|
||||
self.name = spec.get('name', 'xiaomi')
|
||||
self.icon = spec.get('icon', None)
|
||||
|
||||
self.platform = None
|
||||
self.device_class = None
|
||||
self.state_class = None
|
||||
self.icon = None
|
||||
self.external_unit = None
|
||||
self.expression = None
|
||||
|
||||
self.spec_id = hash(f'{self.type_}.{self.iid}')
|
||||
|
||||
@ -510,6 +508,7 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
"""MIoT SPEC property class."""
|
||||
unit: Optional[str]
|
||||
precision: int
|
||||
expr: Optional[str]
|
||||
|
||||
_format_: Type
|
||||
_value_range: Optional[MIoTSpecValueRange]
|
||||
@ -531,7 +530,8 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
unit: Optional[str] = None,
|
||||
value_range: Optional[dict] = None,
|
||||
value_list: Optional[list[dict]] = None,
|
||||
precision: Optional[int] = None
|
||||
precision: Optional[int] = None,
|
||||
expr: Optional[str] = None
|
||||
) -> None:
|
||||
super().__init__(spec=spec)
|
||||
self.service = service
|
||||
@ -541,6 +541,7 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
self.value_range = value_range
|
||||
self.value_list = value_list
|
||||
self.precision = precision or 1
|
||||
self.expr = expr
|
||||
|
||||
self.spec_id = hash(
|
||||
f'p.{self.name}.{self.service.iid}.{self.iid}')
|
||||
@ -613,6 +614,18 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
elif isinstance(value, MIoTSpecValueList):
|
||||
self._value_list = value
|
||||
|
||||
def eval_expr(self, src_value: Any) -> Any:
|
||||
if not self.expr:
|
||||
return src_value
|
||||
try:
|
||||
# pylint: disable=eval-used
|
||||
return eval(self.expr, {'src_value': src_value})
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error(
|
||||
'eval expression error, %s, %s, %s, %s',
|
||||
self.iid, src_value, self.expr, err)
|
||||
return src_value
|
||||
|
||||
def value_format(self, value: Any) -> Any:
|
||||
if value is None:
|
||||
return None
|
||||
@ -639,7 +652,9 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
'value_range': (
|
||||
self._value_range.dump() if self._value_range else None),
|
||||
'value_list': self._value_list.dump() if self._value_list else None,
|
||||
'precision': self.precision
|
||||
'precision': self.precision,
|
||||
'expr': self.expr,
|
||||
'icon': self.icon
|
||||
}
|
||||
|
||||
|
||||
@ -732,7 +747,6 @@ class MIoTSpecService(_MIoTSpecBase):
|
||||
}
|
||||
|
||||
|
||||
# @dataclass
|
||||
class MIoTSpecInstance:
|
||||
"""MIoT SPEC instance class."""
|
||||
urn: str
|
||||
@ -774,7 +788,8 @@ class MIoTSpecInstance:
|
||||
unit=prop['unit'],
|
||||
value_range=prop['value_range'],
|
||||
value_list=prop['value_list'],
|
||||
precision=prop.get('precision', None))
|
||||
precision=prop.get('precision', None),
|
||||
expr=prop.get('expr', None))
|
||||
spec_service.properties.append(spec_prop)
|
||||
for event in service['events']:
|
||||
spec_event = MIoTSpecEvent(
|
||||
@ -1119,6 +1134,79 @@ class _SpecFilter:
|
||||
return False
|
||||
|
||||
|
||||
class _SpecModify:
|
||||
"""MIoT-Spec-V2 modify for entity conversion."""
|
||||
_SPEC_MODIFY_FILE = 'specs/spec_modify.yaml'
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_data: Optional[dict]
|
||||
_selected: Optional[dict]
|
||||
|
||||
def __init__(
|
||||
self, loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> None:
|
||||
self._main_loop = loop or asyncio.get_running_loop()
|
||||
self._data = None
|
||||
|
||||
async def init_async(self) -> None:
|
||||
if isinstance(self._data, dict):
|
||||
return
|
||||
modify_data = None
|
||||
self._data = {}
|
||||
self._selected = None
|
||||
try:
|
||||
modify_data = await self._main_loop.run_in_executor(
|
||||
None, load_yaml_file,
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
self._SPEC_MODIFY_FILE))
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('spec modify, load file error, %s', err)
|
||||
return
|
||||
if not isinstance(modify_data, dict):
|
||||
_LOGGER.error('spec modify, invalid spec modify content')
|
||||
return
|
||||
for key, value in modify_data.items():
|
||||
if not isinstance(key, str) or not isinstance(value, (dict, str)):
|
||||
_LOGGER.error('spec modify, invalid spec modify data')
|
||||
return
|
||||
|
||||
self._data = modify_data
|
||||
|
||||
async def deinit_async(self) -> None:
|
||||
self._data = None
|
||||
self._selected = None
|
||||
|
||||
async def set_spec_async(self, urn: str) -> None:
|
||||
if not self._data:
|
||||
return
|
||||
self._selected = self._data.get(urn, None)
|
||||
if isinstance(self._selected, str):
|
||||
return await self.set_spec_async(urn=self._selected)
|
||||
|
||||
def get_prop_unit(self, siid: int, piid: int) -> Optional[str]:
|
||||
return self.__get_prop_item(siid=siid, piid=piid, key='unit')
|
||||
|
||||
def get_prop_expr(self, siid: int, piid: int) -> Optional[str]:
|
||||
return self.__get_prop_item(siid=siid, piid=piid, key='expr')
|
||||
|
||||
def get_prop_icon(self, siid: int, piid: int) -> Optional[str]:
|
||||
return self.__get_prop_item(siid=siid, piid=piid, key='icon')
|
||||
|
||||
def get_prop_access(self, siid: int, piid: int) -> Optional[list]:
|
||||
access = self.__get_prop_item(siid=siid, piid=piid, key='access')
|
||||
if not isinstance(access, list):
|
||||
return None
|
||||
return access
|
||||
|
||||
def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]:
|
||||
if not self._selected:
|
||||
return None
|
||||
prop = self._selected.get(f'prop.{siid}.{piid}', None)
|
||||
if not prop:
|
||||
return None
|
||||
return prop.get(key, None)
|
||||
|
||||
|
||||
class MIoTSpecParser:
|
||||
"""MIoT SPEC parser."""
|
||||
# pylint: disable=inconsistent-quotes
|
||||
@ -1132,6 +1220,7 @@ class MIoTSpecParser:
|
||||
_multi_lang: _MIoTSpecMultiLang
|
||||
_bool_trans: _SpecBoolTranslation
|
||||
_spec_filter: _SpecFilter
|
||||
_spec_modify: _SpecModify
|
||||
|
||||
_init_done: bool
|
||||
|
||||
@ -1149,6 +1238,7 @@ class MIoTSpecParser:
|
||||
self._bool_trans = _SpecBoolTranslation(
|
||||
lang=self._lang, loop=self._main_loop)
|
||||
self._spec_filter = _SpecFilter(loop=self._main_loop)
|
||||
self._spec_modify = _SpecModify(loop=self._main_loop)
|
||||
|
||||
self._init_done = False
|
||||
|
||||
@ -1157,6 +1247,7 @@ class MIoTSpecParser:
|
||||
return
|
||||
await self._bool_trans.init_async()
|
||||
await self._spec_filter.init_async()
|
||||
await self._spec_modify.init_async()
|
||||
std_lib_cache = await self._storage.load_async(
|
||||
domain=self._DOMAIN, name='spec_std_lib', type_=dict)
|
||||
if (
|
||||
@ -1196,6 +1287,7 @@ class MIoTSpecParser:
|
||||
# self._std_lib.deinit()
|
||||
await self._bool_trans.deinit_async()
|
||||
await self._spec_filter.deinit_async()
|
||||
await self._spec_modify.deinit_async()
|
||||
|
||||
async def parse(
|
||||
self, urn: str, skip_cache: bool = False,
|
||||
@ -1275,6 +1367,8 @@ class MIoTSpecParser:
|
||||
await self._multi_lang.set_spec_async(urn=urn)
|
||||
# Set spec filter
|
||||
await self._spec_filter.set_spec_spec(urn_key=urn_key)
|
||||
# Set spec modify
|
||||
await self._spec_modify.set_spec_async(urn=urn)
|
||||
# Parse device type
|
||||
spec_instance: MIoTSpecInstance = MIoTSpecInstance(
|
||||
urn=urn, name=urn_strs[3],
|
||||
@ -1320,12 +1414,14 @@ class MIoTSpecParser:
|
||||
):
|
||||
continue
|
||||
p_type_strs: list[str] = property_['type'].split(':')
|
||||
# Handle special property.unit
|
||||
unit = property_.get('unit', None)
|
||||
spec_prop: MIoTSpecProperty = MIoTSpecProperty(
|
||||
spec=property_,
|
||||
service=spec_service,
|
||||
format_=property_['format'],
|
||||
access=property_['access'],
|
||||
unit=property_.get('unit', None))
|
||||
unit=unit if unit != 'none' else None)
|
||||
spec_prop.name = p_type_strs[3]
|
||||
# Filter spec property
|
||||
spec_prop.need_filter = (
|
||||
@ -1365,7 +1461,19 @@ class MIoTSpecParser:
|
||||
if v_descriptions:
|
||||
# bool without value-list.name
|
||||
spec_prop.value_list = v_descriptions
|
||||
# Prop modify
|
||||
spec_prop.unit = self._spec_modify.get_prop_unit(
|
||||
siid=service['iid'], piid=property_['iid']
|
||||
) or spec_prop.unit
|
||||
spec_prop.expr = self._spec_modify.get_prop_expr(
|
||||
siid=service['iid'], piid=property_['iid'])
|
||||
spec_prop.icon = self._spec_modify.get_prop_icon(
|
||||
siid=service['iid'], piid=property_['iid'])
|
||||
spec_service.properties.append(spec_prop)
|
||||
custom_access = self._spec_modify.get_prop_access(
|
||||
siid=service['iid'], piid=property_['iid'])
|
||||
if custom_access:
|
||||
spec_prop.access = custom_access
|
||||
# Parse service event
|
||||
for event in service.get('events', []):
|
||||
if (
|
||||
|
@ -59,43 +59,6 @@ data:
|
||||
urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no
|
||||
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
|
||||
translate:
|
||||
contact_state:
|
||||
de:
|
||||
'false': Kein Kontakt
|
||||
'true': Kontakt
|
||||
en:
|
||||
'false': No Contact
|
||||
'true': Contact
|
||||
es:
|
||||
'false': Sin contacto
|
||||
'true': Contacto
|
||||
fr:
|
||||
'false': Pas de contact
|
||||
'true': Contact
|
||||
it:
|
||||
'false': Nessun contatto
|
||||
'true': Contatto
|
||||
ja:
|
||||
'false': 非接触
|
||||
'true': 接触
|
||||
nl:
|
||||
'false': Geen contact
|
||||
'true': Contact
|
||||
pt:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
pt-BR:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
ru:
|
||||
'false': Нет контакта
|
||||
'true': Контакт
|
||||
zh-Hans:
|
||||
'false': 分离
|
||||
'true': 接触
|
||||
zh-Hant:
|
||||
'false': 分離
|
||||
'true': 接觸
|
||||
default:
|
||||
de:
|
||||
'false': Falsch
|
||||
@ -133,6 +96,43 @@ translate:
|
||||
zh-Hant:
|
||||
'false': 假
|
||||
'true': 真
|
||||
contact_state:
|
||||
de:
|
||||
'false': Kein Kontakt
|
||||
'true': Kontakt
|
||||
en:
|
||||
'false': No Contact
|
||||
'true': Contact
|
||||
es:
|
||||
'false': Sin contacto
|
||||
'true': Contacto
|
||||
fr:
|
||||
'false': Pas de contact
|
||||
'true': Contact
|
||||
it:
|
||||
'false': Nessun contatto
|
||||
'true': Contatto
|
||||
ja:
|
||||
'false': 非接触
|
||||
'true': 接触
|
||||
nl:
|
||||
'false': Geen contact
|
||||
'true': Contact
|
||||
pt:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
pt-BR:
|
||||
'false': Sem contato
|
||||
'true': Contato
|
||||
ru:
|
||||
'false': Нет контакта
|
||||
'true': Контакт
|
||||
zh-Hans:
|
||||
'false': 分离
|
||||
'true': 接触
|
||||
zh-Hant:
|
||||
'false': 分離
|
||||
'true': 接觸
|
||||
motion_state:
|
||||
de:
|
||||
'false': Keine Bewegung erkannt
|
||||
|
44
custom_components/xiaomi_home/miot/specs/spec_modify.yaml
Normal file
44
custom_components/xiaomi_home/miot/specs/spec_modify.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:
|
||||
prop.2.1:
|
||||
name: access-mode
|
||||
access:
|
||||
- read
|
||||
- notify
|
||||
prop.2.2:
|
||||
name: ip-address
|
||||
icon: mdi:ip
|
||||
prop.2.3:
|
||||
name: wifi-ssid
|
||||
access:
|
||||
- read
|
||||
- notify
|
||||
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
|
||||
urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1
|
||||
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1:
|
||||
prop.5.1:
|
||||
name: power-consumption
|
||||
expr: round(src_value/1000, 3)
|
||||
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1
|
||||
urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1
|
||||
urn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1:
|
||||
prop.2.2:
|
||||
name: power-consumption
|
||||
expr: round(src_value/1000, 3)
|
||||
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1:
|
||||
prop.11.1:
|
||||
name: power-consumption
|
||||
expr: round(src_value/100, 2)
|
||||
urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1
|
||||
urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816:
|
||||
prop.3.1:
|
||||
name: electric-power
|
||||
expr: round(src_value/100, 2)
|
||||
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:
|
||||
prop.2.1:
|
||||
name: download-speed
|
||||
icon: mdi:download
|
||||
unit: B/s
|
||||
prop.2.2:
|
||||
name: upload-speed
|
||||
icon: mdi:upload
|
||||
unit: B/s
|
@ -50,10 +50,15 @@ from homeassistant.components.sensor import SensorStateClass
|
||||
from homeassistant.components.event import EventDeviceClass
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
LIGHT_LUX,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfTemperature,
|
||||
UnitOfPressure,
|
||||
PERCENTAGE
|
||||
)
|
||||
|
||||
# pylint: disable=pointless-string-statement
|
||||
@ -96,7 +101,7 @@ from homeassistant.const import (
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
SPEC_DEVICE_TRANS_MAP: dict = {
|
||||
'humidifier': {
|
||||
'required': {
|
||||
'humidifier': {
|
||||
@ -263,7 +268,7 @@ SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
SPEC_SERVICE_TRANS_MAP: dict = {
|
||||
'light': {
|
||||
'required': {
|
||||
'properties': {
|
||||
@ -334,15 +339,13 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'<property instance name>':{
|
||||
'device_class': str,
|
||||
'entity': str,
|
||||
'optional':{
|
||||
'state_class': str,
|
||||
'unit_of_measurement': str
|
||||
}
|
||||
'state_class'?: str,
|
||||
'unit_of_measurement'?: str
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
|
||||
SPEC_PROP_TRANS_MAP: dict = {
|
||||
'entities': {
|
||||
'sensor': {
|
||||
'format': {'int', 'float'},
|
||||
@ -356,107 +359,111 @@ SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
|
||||
'properties': {
|
||||
'temperature': {
|
||||
'device_class': SensorDeviceClass.TEMPERATURE,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfTemperature.CELSIUS
|
||||
},
|
||||
'relative-humidity': {
|
||||
'device_class': SensorDeviceClass.HUMIDITY,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': PERCENTAGE
|
||||
},
|
||||
'air-quality-index': {
|
||||
'device_class': SensorDeviceClass.AQI,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
'pm2.5-density': {
|
||||
'device_class': SensorDeviceClass.PM25,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
'pm10-density': {
|
||||
'device_class': SensorDeviceClass.PM10,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
'pm1': {
|
||||
'device_class': SensorDeviceClass.PM1,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
'atmospheric-pressure': {
|
||||
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPressure.PA
|
||||
},
|
||||
'tvoc-density': {
|
||||
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT
|
||||
},
|
||||
'voc-density': 'tvoc-density',
|
||||
'battery-level': {
|
||||
'device_class': SensorDeviceClass.BATTERY,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': PERCENTAGE
|
||||
},
|
||||
'voltage': {
|
||||
'device_class': SensorDeviceClass.VOLTAGE,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricPotential.VOLT
|
||||
}
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricPotential.VOLT
|
||||
},
|
||||
'electric-current': {
|
||||
'device_class': SensorDeviceClass.CURRENT,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
|
||||
}
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
|
||||
},
|
||||
'illumination': {
|
||||
'device_class': SensorDeviceClass.ILLUMINANCE,
|
||||
'entity': 'sensor'
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': LIGHT_LUX
|
||||
},
|
||||
'no-one-determine-time': {
|
||||
'device_class': SensorDeviceClass.DURATION,
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'has-someone-duration': 'no-one-determine-time',
|
||||
'no-one-duration': 'no-one-determine-time',
|
||||
'electric-power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
},
|
||||
'surge-power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
},
|
||||
'power-consumption': {
|
||||
'device_class': SensorDeviceClass.ENERGY,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
},
|
||||
'power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
},
|
||||
'total-battery': {
|
||||
'device_class': SensorDeviceClass.ENERGY,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
},
|
||||
'has-someone-duration': 'no-one-determine-time',
|
||||
'no-one-duration': 'no-one-determine-time'
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||
# Set device_class
|
||||
if self._value_list:
|
||||
self._attr_device_class = SensorDeviceClass.ENUM
|
||||
self._attr_icon = 'mdi:message-text'
|
||||
self._attr_icon = 'mdi:format-text'
|
||||
self._attr_native_unit_of_measurement = None
|
||||
self._attr_options = self._value_list.descriptions
|
||||
else:
|
||||
@ -109,6 +109,9 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||
self._attr_device_class, None) # type: ignore
|
||||
self._attr_native_unit_of_measurement = list(
|
||||
unit_sets)[0] if unit_sets else None
|
||||
# Set suggested precision
|
||||
if spec.format_ in {int, float}:
|
||||
self._attr_suggested_display_precision = spec.precision
|
||||
# Set state_class
|
||||
if spec.state_class:
|
||||
self._attr_state_class = spec.state_class
|
||||
|
@ -20,6 +20,9 @@ SPEC_BOOL_TRANS_FILE = path.join(
|
||||
SPEC_FILTER_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
|
||||
SPEC_MODIFY_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
|
||||
|
||||
|
||||
def load_json_file(file_path: str) -> Optional[dict]:
|
||||
@ -54,7 +57,8 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
|
||||
def save_yaml_file(file_path: str, data: dict) -> None:
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
yaml.safe_dump(
|
||||
data, file, default_flow_style=False, allow_unicode=True, indent=2)
|
||||
data, file, default_flow_style=False,
|
||||
allow_unicode=True, indent=2, sort_keys=False)
|
||||
|
||||
|
||||
def dict_str_str(d: dict) -> bool:
|
||||
@ -135,6 +139,21 @@ def bool_trans(d: dict) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def spec_modify(data: dict) -> bool:
|
||||
"""dict[str, str | dict[str, dict]]"""
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
for urn, content in data.items():
|
||||
if not isinstance(urn, str) or not isinstance(content, (dict, str)):
|
||||
return False
|
||||
if isinstance(content, str):
|
||||
continue
|
||||
for key, value in content.items():
|
||||
if not isinstance(key, str) or not isinstance(value, dict):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
|
||||
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
|
||||
_LOGGER.info('invalid type')
|
||||
@ -181,6 +200,12 @@ def sort_spec_filter(file_path: str):
|
||||
return filter_data
|
||||
|
||||
|
||||
def sort_spec_modify(file_path: str):
|
||||
filter_data = load_yaml_file(file_path=file_path)
|
||||
assert isinstance(filter_data, dict), f'{file_path} format error'
|
||||
return dict(sorted(filter_data.items()))
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_bool_trans():
|
||||
data = load_yaml_file(SPEC_BOOL_TRANS_FILE)
|
||||
@ -197,6 +222,14 @@ def test_spec_filter():
|
||||
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_spec_modify():
|
||||
data = load_yaml_file(SPEC_MODIFY_FILE)
|
||||
assert isinstance(data, dict)
|
||||
assert data, f'load {SPEC_MODIFY_FILE} failed'
|
||||
assert spec_modify(data), f'{SPEC_MODIFY_FILE} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_miot_i18n():
|
||||
for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
|
||||
@ -286,3 +319,6 @@ def test_sort_spec_data():
|
||||
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
|
||||
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
|
||||
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
|
||||
sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE)
|
||||
save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data)
|
||||
_LOGGER.info('%s formatted.', SPEC_MODIFY_FILE)
|
||||
|
Loading…
x
Reference in New Issue
Block a user