refactor: refactor miot device and spec (#592)

* 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
This commit is contained in:
Paul Shawn
2025-01-17 18:14:31 +08:00
committed by GitHub
parent bf116e13a4
commit ef56448dbb
54 changed files with 2050 additions and 1978 deletions

View File

@ -75,7 +75,7 @@ from homeassistant.const import (
)
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.util import slugify
# pylint: disable=relative-beyond-top-level
from .specs.specv2entity import (
@ -85,6 +85,7 @@ from .specs.specv2entity import (
SPEC_PROP_TRANS_MAP,
SPEC_SERVICE_TRANS_MAP
)
from .common import slugify_name, slugify_did
from .const import DOMAIN
from .miot_client import MIoTClient
from .miot_error import MIoTClientError, MIoTDeviceError
@ -94,7 +95,9 @@ from .miot_spec import (
MIoTSpecEvent,
MIoTSpecInstance,
MIoTSpecProperty,
MIoTSpecService
MIoTSpecService,
MIoTSpecValueList,
MIoTSpecValueRange
)
_LOGGER = logging.getLogger(__name__)
@ -142,9 +145,12 @@ class MIoTDevice:
_room_id: str
_room_name: str
_suggested_area: str
_suggested_area: Optional[str]
_device_state_sub_list: dict[str, Callable[[str, MIoTDeviceState], None]]
_sub_id: int
_device_state_sub_list: dict[str, dict[
str, Callable[[str, MIoTDeviceState], None]]]
_value_sub_list: dict[str, dict[str, Callable[[dict, Any], None]]]
_entity_list: dict[str, list[MIoTEntityData]]
_prop_list: dict[str, list[MIoTSpecProperty]]
@ -153,7 +159,7 @@ class MIoTDevice:
def __init__(
self, miot_client: MIoTClient,
device_info: dict[str, str],
device_info: dict[str, Any],
spec_instance: MIoTSpecInstance
) -> None:
self.miot_client = miot_client
@ -183,7 +189,9 @@ class MIoTDevice:
case _:
self._suggested_area = None
self._sub_id = 0
self._device_state_sub_list = {}
self._value_sub_list = {}
self._entity_list = {}
self._prop_list = {}
self._event_list = {}
@ -234,36 +242,76 @@ class MIoTDevice:
def sub_device_state(
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
) -> bool:
self._device_state_sub_list[key] = handler
return True
) -> int:
self._sub_id += 1
if key in self._device_state_sub_list:
self._device_state_sub_list[key][str(self._sub_id)] = handler
else:
self._device_state_sub_list[key] = {str(self._sub_id): handler}
return self._sub_id
def unsub_device_state(self, key: str) -> bool:
self._device_state_sub_list.pop(key, None)
return True
def unsub_device_state(self, key: str, sub_id: int) -> None:
sub_list = self._device_state_sub_list.get(key, None)
if sub_list:
sub_list.pop(str(sub_id), None)
if not sub_list:
self._device_state_sub_list.pop(key, None)
def sub_property(
self, handler: Callable[[dict, Any], None], siid: int = None,
piid: int = None, handler_ctx: Any = None
) -> bool:
return self.miot_client.sub_prop(
did=self._did, handler=handler, siid=siid, piid=piid,
handler_ctx=handler_ctx)
self, handler: Callable[[dict, Any], None], siid: int, piid: int
) -> int:
key: str = f'p.{siid}.{piid}'
def unsub_property(self, siid: int = None, piid: int = None) -> bool:
return self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)
def _on_prop_changed(params: dict, ctx: Any) -> None:
for handler in self._value_sub_list[key].values():
handler(params, ctx)
self._sub_id += 1
if key in self._value_sub_list:
self._value_sub_list[key][str(self._sub_id)] = handler
else:
self._value_sub_list[key] = {str(self._sub_id): handler}
self.miot_client.sub_prop(
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
return self._sub_id
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
key: str = f'p.{siid}.{piid}'
sub_list = self._value_sub_list.get(key, None)
if sub_list:
sub_list.pop(str(sub_id), None)
if not sub_list:
self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)
self._value_sub_list.pop(key, None)
def sub_event(
self, handler: Callable[[dict, Any], None], siid: int = None,
eiid: int = None, handler_ctx: Any = None
) -> bool:
return self.miot_client.sub_event(
did=self._did, handler=handler, siid=siid, eiid=eiid,
handler_ctx=handler_ctx)
self, handler: Callable[[dict, Any], None], siid: int, eiid: int
) -> int:
key: str = f'e.{siid}.{eiid}'
def unsub_event(self, siid: int = None, eiid: int = None) -> bool:
return self.miot_client.unsub_event(
did=self._did, siid=siid, eiid=eiid)
def _on_event_occurred(params: dict, ctx: Any) -> None:
for handler in self._value_sub_list[key].values():
handler(params, ctx)
self._sub_id += 1
if key in self._value_sub_list:
self._value_sub_list[key][str(self._sub_id)] = handler
else:
self._value_sub_list[key] = {str(self._sub_id): handler}
self.miot_client.sub_event(
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
return self._sub_id
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
key: str = f'e.{siid}.{eiid}'
sub_list = self._value_sub_list.get(key, None)
if sub_list:
sub_list.pop(str(sub_id), None)
if not sub_list:
self.miot_client.unsub_event(did=self._did, siid=siid, eiid=eiid)
self._value_sub_list.pop(key, None)
@property
def device_info(self) -> DeviceInfo:
@ -287,11 +335,8 @@ class MIoTDevice:
@property
def did_tag(self) -> str:
return slugify(f'{self.miot_client.cloud_server}_{self._did}')
@staticmethod
def gen_did_tag(cloud_server: str, did: str) -> str:
return slugify(f'{cloud_server}_{did}')
return slugify_did(
cloud_server=self.miot_client.cloud_server, did=self._did)
def gen_device_entity_id(self, ha_domain: str) -> str:
return (
@ -308,21 +353,24 @@ class MIoTDevice:
) -> str:
return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_p_{siid}_{piid}')
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
f'_p_{siid}_{piid}')
def gen_event_entity_id(
self, ha_domain: str, spec_name: str, siid: int, eiid: int
) -> str:
return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_e_{siid}_{eiid}')
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
f'_e_{siid}_{eiid}')
def gen_action_entity_id(
self, ha_domain: str, spec_name: str, siid: int, aiid: int
) -> str:
return (
f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'
f'{self._model_strs[-1][:20]}_{slugify(spec_name)}_a_{siid}_{aiid}')
f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'
f'_a_{siid}_{aiid}')
@property
def name(self) -> str:
@ -341,14 +389,20 @@ class MIoTDevice:
self._entity_list[entity_data.platform].append(entity_data)
def append_prop(self, prop: MIoTSpecProperty) -> None:
if not prop.platform:
return
self._prop_list.setdefault(prop.platform, [])
self._prop_list[prop.platform].append(prop)
def append_event(self, event: MIoTSpecEvent) -> None:
if not event.platform:
return
self._event_list.setdefault(event.platform, [])
self._event_list[event.platform].append(event)
def append_action(self, action: MIoTSpecAction) -> None:
if not action.platform:
return
self._action_list.setdefault(action.platform, [])
self._action_list[action.platform].append(action)
@ -507,7 +561,7 @@ class MIoTDevice:
if prop_access != (SPEC_PROP_TRANS_MAP[
'entities'][platform]['access']):
return None
if prop.format_ not in SPEC_PROP_TRANS_MAP[
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
'entities'][platform]['format']:
return None
if prop.unit:
@ -560,9 +614,9 @@ class MIoTDevice:
# general conversion
if not prop.platform:
if prop.writable:
if prop.format_ == 'str':
if prop.format_ == str:
prop.platform = 'text'
elif prop.format_ == 'bool':
elif prop.format_ == bool:
prop.platform = 'switch'
prop.device_class = SwitchDeviceClass.SWITCH
elif prop.value_list:
@ -573,9 +627,11 @@ class MIoTDevice:
# Irregular property will not be transformed.
pass
elif prop.readable or prop.notifiable:
prop.platform = 'sensor'
if prop.platform:
self.append_prop(prop=prop)
if prop.format_ == bool:
prop.platform = 'binary_sensor'
else:
prop.platform = 'sensor'
self.append_prop(prop=prop)
# STEP 3.2: event conversion
for event in service.events:
if event.platform:
@ -703,10 +759,11 @@ class MIoTDevice:
def __on_device_state_changed(
self, did: str, state: MIoTDeviceState, ctx: Any
) -> None:
self._online = state
for key, handler in self._device_state_sub_list.items():
self.miot_client.main_loop.call_soon_threadsafe(
handler, key, state)
self._online = state == MIoTDeviceState.ONLINE
for key, sub_list in self._device_state_sub_list.items():
for handler in sub_list.values():
self.miot_client.main_loop.call_soon_threadsafe(
handler, key, state)
class MIoTServiceEntity(Entity):
@ -718,8 +775,11 @@ class MIoTServiceEntity(Entity):
_main_loop: asyncio.AbstractEventLoop
_prop_value_map: dict[MIoTSpecProperty, Any]
_state_sub_id: int
_value_sub_ids: dict[str, int]
_event_occurred_handler: Callable[[MIoTSpecEvent, dict], None]
_event_occurred_handler: Optional[
Callable[[MIoTSpecEvent, dict], None]]
_prop_changed_subs: dict[
MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]]
@ -738,13 +798,15 @@ class MIoTServiceEntity(Entity):
self.entity_data = entity_data
self._main_loop = miot_device.miot_client.main_loop
self._prop_value_map = {}
self._state_sub_id = 0
self._value_sub_ids = {}
# Gen entity id
if isinstance(entity_data.spec, MIoTSpecInstance):
if isinstance(self.entity_data.spec, MIoTSpecInstance):
self.entity_id = miot_device.gen_device_entity_id(DOMAIN)
self._attr_name = f' {self.entity_data.spec.description_trans}'
elif isinstance(entity_data.spec, MIoTSpecService):
elif isinstance(self.entity_data.spec, MIoTSpecService):
self.entity_id = miot_device.gen_service_entity_id(
DOMAIN, siid=entity_data.spec.iid)
DOMAIN, siid=self.entity_data.spec.iid)
self._attr_name = (
f'{"* "if self.entity_data.spec.proprietary else " "}'
f'{self.entity_data.spec.description_trans}')
@ -763,7 +825,9 @@ class MIoTServiceEntity(Entity):
self.entity_id)
@property
def event_occurred_handler(self) -> Callable[[MIoTSpecEvent, dict], None]:
def event_occurred_handler(
self
) -> Optional[Callable[[MIoTSpecEvent, dict], None]]:
return self._event_occurred_handler
@event_occurred_handler.setter
@ -784,25 +848,27 @@ class MIoTServiceEntity(Entity):
self._prop_changed_subs.pop(prop, None)
@property
def device_info(self) -> dict:
def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info
async def async_added_to_hass(self) -> None:
state_id = 's.0'
if isinstance(self.entity_data.spec, MIoTSpecService):
state_id = f's.{self.entity_data.spec.iid}'
self.miot_device.sub_device_state(
self._state_sub_id = self.miot_device.sub_device_state(
key=state_id, handler=self.__on_device_state_changed)
# Sub prop
for prop in self.entity_data.props:
if not prop.notifiable and not prop.readable:
continue
self.miot_device.sub_property(
key = f'p.{prop.service.iid}.{prop.iid}'
self._value_sub_ids[key] = self.miot_device.sub_property(
handler=self.__on_properties_changed,
siid=prop.service.iid, piid=prop.iid)
# Sub event
for event in self.entity_data.events:
self.miot_device.sub_event(
key = f'e.{event.service.iid}.{event.iid}'
self._value_sub_ids[key] = self.miot_device.sub_event(
handler=self.__on_event_occurred,
siid=event.service.iid, eiid=event.iid)
@ -817,30 +883,39 @@ class MIoTServiceEntity(Entity):
state_id = 's.0'
if isinstance(self.entity_data.spec, MIoTSpecService):
state_id = f's.{self.entity_data.spec.iid}'
self.miot_device.unsub_device_state(key=state_id)
self.miot_device.unsub_device_state(
key=state_id, sub_id=self._state_sub_id)
# Unsub prop
for prop in self.entity_data.props:
if not prop.notifiable and not prop.readable:
continue
self.miot_device.unsub_property(
siid=prop.service.iid, piid=prop.iid)
sub_id = self._value_sub_ids.pop(
f'p.{prop.service.iid}.{prop.iid}', None)
if sub_id:
self.miot_device.unsub_property(
siid=prop.service.iid, piid=prop.iid, sub_id=sub_id)
# Unsub event
for event in self.entity_data.events:
self.miot_device.unsub_event(
siid=event.service.iid, eiid=event.iid)
sub_id = self._value_sub_ids.pop(
f'e.{event.service.iid}.{event.iid}', None)
if sub_id:
self.miot_device.unsub_event(
siid=event.service.iid, eiid=event.iid, sub_id=sub_id)
def get_map_description(self, map_: dict[int, Any], key: int) -> Any:
def get_map_value(
self, map_: dict[int, Any], key: int
) -> Any:
if map_ is None:
return None
return map_.get(key, None)
def get_map_value(
self, map_: dict[int, Any], description: Any
def get_map_key(
self, map_: dict[int, Any], value: Any
) -> Optional[int]:
if map_ is None:
return None
for key, value in map_.items():
if value == description:
for key, value_ in map_.items():
if value_ == value:
return key
return None
@ -999,11 +1074,12 @@ class MIoTPropertyEntity(Entity):
service: MIoTSpecService
_main_loop: asyncio.AbstractEventLoop
# {'min':int, 'max':int, 'step': int}
_value_range: dict[str, int]
_value_range: Optional[MIoTSpecValueRange]
# {Any: Any}
_value_list: dict[Any, Any]
_value_list: Optional[MIoTSpecValueList]
_value: Any
_state_sub_id: int
_value_sub_id: int
_pending_write_ha_state_timer: Optional[asyncio.TimerHandle]
@ -1015,12 +1091,10 @@ class MIoTPropertyEntity(Entity):
self.service = spec.service
self._main_loop = miot_device.miot_client.main_loop
self._value_range = spec.value_range
if spec.value_list:
self._value_list = {
item['value']: item['description'] for item in spec.value_list}
else:
self._value_list = None
self._value_list = spec.value_list
self._value = None
self._state_sub_id = 0
self._value_sub_id = 0
self._pending_write_ha_state_timer = None
# Gen entity_id
self.entity_id = self.miot_device.gen_prop_entity_id(
@ -1042,16 +1116,16 @@ class MIoTPropertyEntity(Entity):
self._value_list)
@property
def device_info(self) -> dict:
def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info
async def async_added_to_hass(self) -> None:
# Sub device state changed
self.miot_device.sub_device_state(
self._state_sub_id = self.miot_device.sub_device_state(
key=f'{ self.service.iid}.{self.spec.iid}',
handler=self.__on_device_state_changed)
# Sub value changed
self.miot_device.sub_property(
self._value_sub_id = self.miot_device.sub_property(
handler=self.__on_value_changed,
siid=self.service.iid, piid=self.spec.iid)
# Refresh value
@ -1063,22 +1137,21 @@ class MIoTPropertyEntity(Entity):
self._pending_write_ha_state_timer.cancel()
self._pending_write_ha_state_timer = None
self.miot_device.unsub_device_state(
key=f'{ self.service.iid}.{self.spec.iid}')
key=f'{ self.service.iid}.{self.spec.iid}',
sub_id=self._state_sub_id)
self.miot_device.unsub_property(
siid=self.service.iid, piid=self.spec.iid)
siid=self.service.iid, piid=self.spec.iid,
sub_id=self._value_sub_id)
def get_vlist_description(self, value: Any) -> str:
def get_vlist_description(self, value: Any) -> Optional[str]:
if not self._value_list:
return None
return self._value_list.get(value, None)
return self._value_list.get_description_by_value(value)
def get_vlist_value(self, description: str) -> Any:
if not self._value_list:
return None
for key, value in self._value_list.items():
if value == description:
return key
return None
return self._value_list.get_value_by_description(description)
async def set_property_async(self, value: Any) -> bool:
if not self.spec.writable:
@ -1148,9 +1221,10 @@ class MIoTEventEntity(Entity):
service: MIoTSpecService
_main_loop: asyncio.AbstractEventLoop
_value: Any
_attr_event_types: list[str]
_arguments_map: dict[int, str]
_state_sub_id: int
_value_sub_id: int
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None:
if miot_device is None or spec is None or spec.service is None:
@ -1159,7 +1233,6 @@ class MIoTEventEntity(Entity):
self.spec = spec
self.service = spec.service
self._main_loop = miot_device.miot_client.main_loop
self._value = None
# Gen entity_id
self.entity_id = self.miot_device.gen_event_entity_id(
ha_domain=DOMAIN, spec_name=spec.name,
@ -1177,6 +1250,8 @@ class MIoTEventEntity(Entity):
self._arguments_map = {}
for prop in spec.argument:
self._arguments_map[prop.iid] = prop.description_trans
self._state_sub_id = 0
self._value_sub_id = 0
_LOGGER.info(
'new miot event entity, %s, %s, %s, %s, %s',
@ -1184,29 +1259,31 @@ class MIoTEventEntity(Entity):
spec.device_class, self.entity_id)
@property
def device_info(self) -> dict:
def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info
async def async_added_to_hass(self) -> None:
# Sub device state changed
self.miot_device.sub_device_state(
self._state_sub_id = self.miot_device.sub_device_state(
key=f'event.{ self.service.iid}.{self.spec.iid}',
handler=self.__on_device_state_changed)
# Sub value changed
self.miot_device.sub_event(
self._value_sub_id = self.miot_device.sub_event(
handler=self.__on_event_occurred,
siid=self.service.iid, eiid=self.spec.iid)
async def async_will_remove_from_hass(self) -> None:
self.miot_device.unsub_device_state(
key=f'event.{ self.service.iid}.{self.spec.iid}')
key=f'event.{ self.service.iid}.{self.spec.iid}',
sub_id=self._state_sub_id)
self.miot_device.unsub_event(
siid=self.service.iid, eiid=self.spec.iid)
siid=self.service.iid, eiid=self.spec.iid,
sub_id=self._value_sub_id)
@abstractmethod
def on_event_occurred(
self, name: str, arguments: list[dict[int, Any]]
): ...
self, name: str, arguments: dict[str, Any] | None = None
) -> None: ...
def __on_event_occurred(self, params: dict, ctx: Any) -> None:
_LOGGER.debug('event occurred, %s', params)
@ -1253,11 +1330,11 @@ class MIoTActionEntity(Entity):
miot_device: MIoTDevice
spec: MIoTSpecAction
service: MIoTSpecService
action_platform: str
_main_loop: asyncio.AbstractEventLoop
_in_map: dict[int, MIoTSpecProperty]
_out_map: dict[int, MIoTSpecProperty]
_state_sub_id: int
def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:
if miot_device is None or spec is None or spec.service is None:
@ -1265,8 +1342,8 @@ class MIoTActionEntity(Entity):
self.miot_device = miot_device
self.spec = spec
self.service = spec.service
self.action_platform = 'action'
self._main_loop = miot_device.miot_client.main_loop
self._state_sub_id = 0
# Gen entity_id
self.entity_id = self.miot_device.gen_action_entity_id(
ha_domain=DOMAIN, spec_name=spec.name,
@ -1286,19 +1363,22 @@ class MIoTActionEntity(Entity):
spec.device_class, self.entity_id)
@property
def device_info(self) -> dict:
def device_info(self) -> Optional[DeviceInfo]:
return self.miot_device.device_info
async def async_added_to_hass(self) -> None:
self.miot_device.sub_device_state(
key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}',
self._state_sub_id = self.miot_device.sub_device_state(
key=f'a.{ self.service.iid}.{self.spec.iid}',
handler=self.__on_device_state_changed)
async def async_will_remove_from_hass(self) -> None:
self.miot_device.unsub_device_state(
key=f'{self.action_platform}.{ self.service.iid}.{self.spec.iid}')
key=f'a.{ self.service.iid}.{self.spec.iid}',
sub_id=self._state_sub_id)
async def action_async(self, in_list: list = None) -> Optional[list]:
async def action_async(
self, in_list: Optional[list] = None
) -> Optional[list]:
try:
return await self.miot_device.miot_client.action_async(
did=self.miot_device.did,