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:
Paul Shawn
2025-01-22 19:21:02 +08:00
committed by GitHub
parent 3c16f0ffbb
commit 8778b00c3a
11 changed files with 598 additions and 309 deletions

View File

@ -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