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

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