mirror of
				https://github.com/XiaoMi/ha_xiaomi_home.git
				synced 2025-10-31 17:32:20 +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:
		| @@ -258,13 +258,14 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity): | |||||||
|                     f'{self.entity_id}') |                     f'{self.entity_id}') | ||||||
|             return |             return | ||||||
|         # set air-conditioner on |         # set air-conditioner on | ||||||
|         elif self.get_prop_value(prop=self._prop_on) is False: |         if self.get_prop_value(prop=self._prop_on) is False: | ||||||
|             await self.set_property_async(prop=self._prop_on, value=True) |             await self.set_property_async( | ||||||
|  |                 prop=self._prop_on, value=True, update=False) | ||||||
|         # set mode |         # set mode | ||||||
|         mode_value = self.get_map_key( |         mode_value = self.get_map_key( | ||||||
|             map_=self._hvac_mode_map, value=hvac_mode) |             map_=self._hvac_mode_map, value=hvac_mode) | ||||||
|         if ( |         if ( | ||||||
|             mode_value is None or |             not mode_value or | ||||||
|             not await self.set_property_async( |             not await self.set_property_async( | ||||||
|                 prop=self._prop_mode, value=mode_value) |                 prop=self._prop_mode, value=mode_value) | ||||||
|         ): |         ): | ||||||
| @@ -368,9 +369,9 @@ class AirConditioner(MIoTServiceEntity, ClimateEntity): | |||||||
|         """Return the hvac mode. e.g., heat, cool mode.""" |         """Return the hvac mode. e.g., heat, cool mode.""" | ||||||
|         if self.get_prop_value(prop=self._prop_on) is False: |         if self.get_prop_value(prop=self._prop_on) is False: | ||||||
|             return HVACMode.OFF |             return HVACMode.OFF | ||||||
|         return self.get_map_key( |         return self.get_map_value( | ||||||
|             map_=self._hvac_mode_map, |             map_=self._hvac_mode_map, | ||||||
|             value=self.get_prop_value(prop=self._prop_mode)) |             key=self.get_prop_value(prop=self._prop_mode)) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def fan_mode(self) -> Optional[str]: |     def fan_mode(self) -> Optional[str]: | ||||||
|   | |||||||
| @@ -150,7 +150,7 @@ class MIoTClient: | |||||||
|     # Device list update timestamp |     # Device list update timestamp | ||||||
|     _device_list_update_ts: int |     _device_list_update_ts: int | ||||||
|  |  | ||||||
|     _sub_source_list: dict[str, str] |     _sub_source_list: dict[str, Optional[str]] | ||||||
|     _sub_tree: MIoTMatcher |     _sub_tree: MIoTMatcher | ||||||
|     _sub_device_state: dict[str, MipsDeviceState] |     _sub_device_state: dict[str, MipsDeviceState] | ||||||
|  |  | ||||||
| @@ -620,7 +620,7 @@ class MIoTClient: | |||||||
|         # Priority local control |         # Priority local control | ||||||
|         if self._ctrl_mode == CtrlMode.AUTO: |         if self._ctrl_mode == CtrlMode.AUTO: | ||||||
|             # Gateway control |             # Gateway control | ||||||
|             device_gw: dict = self._device_list_gateway.get(did, None) |             device_gw = self._device_list_gateway.get(did, None) | ||||||
|             if ( |             if ( | ||||||
|                 device_gw and device_gw.get('online', False) |                 device_gw and device_gw.get('online', False) | ||||||
|                 and device_gw.get('specv2_access', False) |                 and device_gw.get('specv2_access', False) | ||||||
| @@ -641,7 +641,7 @@ class MIoTClient: | |||||||
|                     raise MIoTClientError( |                     raise MIoTClientError( | ||||||
|                         self.__get_exec_error_with_rc(rc=rc)) |                         self.__get_exec_error_with_rc(rc=rc)) | ||||||
|             # Lan control |             # 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): |             if device_lan and device_lan.get('online', False): | ||||||
|                 result = await self._miot_lan.set_prop_async( |                 result = await self._miot_lan.set_prop_async( | ||||||
|                     did=did, siid=siid, piid=piid, value=value) |                     did=did, siid=siid, piid=piid, value=value) | ||||||
| @@ -657,7 +657,7 @@ class MIoTClient: | |||||||
|         # Cloud control |         # Cloud control | ||||||
|         device_cloud = self._device_list_cloud.get(did, None) |         device_cloud = self._device_list_cloud.get(did, None) | ||||||
|         if device_cloud and device_cloud.get('online', False): |         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=[ |                 params=[ | ||||||
|                     {'did': did, 'siid': siid, 'piid': piid, 'value': value} |                     {'did': did, 'siid': siid, 'piid': piid, 'value': value} | ||||||
|                 ]) |                 ]) | ||||||
| @@ -746,7 +746,7 @@ class MIoTClient: | |||||||
|         if did not in self._device_list_cache: |         if did not in self._device_list_cache: | ||||||
|             raise MIoTClientError(f'did not exist, {did}') |             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 |         # Priority local control | ||||||
|         if self._ctrl_mode == CtrlMode.AUTO: |         if self._ctrl_mode == CtrlMode.AUTO: | ||||||
|             if ( |             if ( | ||||||
| @@ -782,7 +782,7 @@ class MIoTClient: | |||||||
|                     self.__get_exec_error_with_rc(rc=rc)) |                     self.__get_exec_error_with_rc(rc=rc)) | ||||||
|         # Cloud control |         # Cloud control | ||||||
|         device_cloud = self._device_list_cloud.get(did, None) |         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( |             result: dict = await self._http.action_async( | ||||||
|                 did=did, siid=siid, aiid=aiid, in_list=in_list) |                 did=did, siid=siid, aiid=aiid, in_list=in_list) | ||||||
|             if result: |             if result: | ||||||
| @@ -798,14 +798,15 @@ class MIoTClient: | |||||||
|                             dids=[did])) |                             dids=[did])) | ||||||
|                 raise MIoTClientError( |                 raise MIoTClientError( | ||||||
|                     self.__get_exec_error_with_rc(rc=rc)) |                     self.__get_exec_error_with_rc(rc=rc)) | ||||||
|         # Show error message |         # TODO: Show error message | ||||||
|         _LOGGER.error( |         _LOGGER.error( | ||||||
|             'client action failed, %s.%d.%d', did, siid, aiid) |             'client action failed, %s.%d.%d', did, siid, aiid) | ||||||
|         return None |         return [] | ||||||
|  |  | ||||||
|     def sub_prop( |     def sub_prop( | ||||||
|         self, did: str, handler: Callable[[dict, Any], None], |         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: |     ) -> bool: | ||||||
|         if did not in self._device_list_cache: |         if did not in self._device_list_cache: | ||||||
|             raise MIoTClientError(f'did not exist, {did}') |             raise MIoTClientError(f'did not exist, {did}') | ||||||
| @@ -818,7 +819,9 @@ class MIoTClient: | |||||||
|         _LOGGER.debug('client sub prop, %s', topic) |         _LOGGER.debug('client sub prop, %s', topic) | ||||||
|         return True |         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 = ( |         topic = ( | ||||||
|             f'{did}/p/' |             f'{did}/p/' | ||||||
|             f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}') |             f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}') | ||||||
| @@ -829,7 +832,8 @@ class MIoTClient: | |||||||
|  |  | ||||||
|     def sub_event( |     def sub_event( | ||||||
|         self, did: str, handler: Callable[[dict, Any], None], |         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: |     ) -> bool: | ||||||
|         if did not in self._device_list_cache: |         if did not in self._device_list_cache: | ||||||
|             raise MIoTClientError(f'did not exist, {did}') |             raise MIoTClientError(f'did not exist, {did}') | ||||||
| @@ -841,7 +845,9 @@ class MIoTClient: | |||||||
|         _LOGGER.debug('client sub event, %s', topic) |         _LOGGER.debug('client sub event, %s', topic) | ||||||
|         return True |         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 = ( |         topic = ( | ||||||
|             f'{did}/e/' |             f'{did}/e/' | ||||||
|             f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}') |             f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}') | ||||||
| @@ -1081,7 +1087,7 @@ class MIoTClient: | |||||||
|                 if state_old == state_new: |                 if state_old == state_new: | ||||||
|                     continue |                     continue | ||||||
|                 self._device_list_cache[did]['online'] = state_new |                 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: |                 if sub and sub.handler: | ||||||
|                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) |                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) | ||||||
|             self.__request_show_devices_changed_notify() |             self.__request_show_devices_changed_notify() | ||||||
| @@ -1091,8 +1097,8 @@ class MIoTClient: | |||||||
|         self, group_id: str, state: bool |         self, group_id: str, state: bool | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         _LOGGER.info('local mips state changed, %s, %s', group_id, state) |         _LOGGER.info('local mips state changed, %s, %s', group_id, state) | ||||||
|         mips: MipsLocalClient = self._mips_local.get(group_id, None) |         mips = self._mips_local.get(group_id, None) | ||||||
|         if mips is None: |         if not mips: | ||||||
|             _LOGGER.error( |             _LOGGER.error( | ||||||
|                 'local mips state changed, mips not exist, %s', group_id) |                 'local mips state changed, mips not exist, %s', group_id) | ||||||
|             return |             return | ||||||
| @@ -1124,7 +1130,7 @@ class MIoTClient: | |||||||
|                 if state_old == state_new: |                 if state_old == state_new: | ||||||
|                     continue |                     continue | ||||||
|                 self._device_list_cache[did]['online'] = state_new |                 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: |                 if sub and sub.handler: | ||||||
|                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) |                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) | ||||||
|             self.__request_show_devices_changed_notify() |             self.__request_show_devices_changed_notify() | ||||||
| @@ -1171,7 +1177,7 @@ class MIoTClient: | |||||||
|                 if state_old == state_new: |                 if state_old == state_new: | ||||||
|                     continue |                     continue | ||||||
|                 self._device_list_cache[did]['online'] = state_new |                 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: |                 if sub and sub.handler: | ||||||
|                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) |                     sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx) | ||||||
|             self._device_list_lan = {} |             self._device_list_lan = {} | ||||||
| @@ -1201,7 +1207,7 @@ class MIoTClient: | |||||||
|         if state_old == state_new: |         if state_old == state_new: | ||||||
|             return |             return | ||||||
|         self._device_list_cache[did]['online'] = state_new |         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: |         if sub and sub.handler: | ||||||
|             sub.handler( |             sub.handler( | ||||||
|                 did, MIoTDeviceState.ONLINE if state_new |                 did, MIoTDeviceState.ONLINE if state_new | ||||||
| @@ -1257,7 +1263,7 @@ class MIoTClient: | |||||||
|         if state_old == state_new: |         if state_old == state_new: | ||||||
|             return |             return | ||||||
|         self._device_list_cache[did]['online'] = state_new |         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: |         if sub and sub.handler: | ||||||
|             sub.handler( |             sub.handler( | ||||||
|                 did, MIoTDeviceState.ONLINE if state_new |                 did, MIoTDeviceState.ONLINE if state_new | ||||||
| @@ -1301,9 +1307,8 @@ class MIoTClient: | |||||||
|     async def __load_cache_device_async(self) -> None: |     async def __load_cache_device_async(self) -> None: | ||||||
|         """Load device list from cache.""" |         """Load device list from cache.""" | ||||||
|         cache_list: Optional[dict[str, dict]] = await self._storage.load_async( |         cache_list: Optional[dict[str, dict]] = await self._storage.load_async( | ||||||
|             domain='miot_devices', |             domain='miot_devices', name=f'{self._uid}_{self._cloud_server}', | ||||||
|             name=f'{self._uid}_{self._cloud_server}', |             type_=dict)  # type: ignore | ||||||
|             type_=dict) |  | ||||||
|         if not cache_list: |         if not cache_list: | ||||||
|             self.__show_client_error_notify( |             self.__show_client_error_notify( | ||||||
|                 message=self._i18n.translate( |                 message=self._i18n.translate( | ||||||
| @@ -1346,7 +1351,7 @@ class MIoTClient: | |||||||
|             cloud_state_old: Optional[bool] = self._device_list_cloud.get( |             cloud_state_old: Optional[bool] = self._device_list_cloud.get( | ||||||
|                 did, {}).get('online', None) |                 did, {}).get('online', None) | ||||||
|             cloud_state_new: Optional[bool] = 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: |             if device_new: | ||||||
|                 cloud_state_new = device_new.get('online', None) |                 cloud_state_new = device_new.get('online', None) | ||||||
|                 # Update cache device info |                 # Update cache device info | ||||||
| @@ -1371,7 +1376,7 @@ class MIoTClient: | |||||||
|                 continue |                 continue | ||||||
|             info['online'] = state_new |             info['online'] = state_new | ||||||
|             # Call device state changed callback |             # 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: |             if sub and sub.handler: | ||||||
|                 sub.handler( |                 sub.handler( | ||||||
|                     did, MIoTDeviceState.ONLINE if state_new |                     did, MIoTDeviceState.ONLINE if state_new | ||||||
| @@ -1426,8 +1431,7 @@ class MIoTClient: | |||||||
|         self, dids: list[str] |         self, dids: list[str] | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         _LOGGER.debug('refresh cloud device with dids, %s', dids) |         _LOGGER.debug('refresh cloud device with dids, %s', dids) | ||||||
|         cloud_list: dict[str, dict] = ( |         cloud_list = await self._http.get_devices_with_dids_async(dids=dids) | ||||||
|             await self._http.get_devices_with_dids_async(dids=dids)) |  | ||||||
|         if cloud_list is None: |         if cloud_list is None: | ||||||
|             _LOGGER.error('cloud http get_dev_list_async failed, %s', dids) |             _LOGGER.error('cloud http get_dev_list_async failed, %s', dids) | ||||||
|             return |             return | ||||||
| @@ -1466,11 +1470,11 @@ class MIoTClient: | |||||||
|         for did, info in self._device_list_cache.items(): |         for did, info in self._device_list_cache.items(): | ||||||
|             if did not in filter_dids: |             if did not in filter_dids: | ||||||
|                 continue |                 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( |             gw_state_old = device_old.get( | ||||||
|                 'online', False) if device_old else False |                 'online', False) if device_old else False | ||||||
|             gw_state_new: bool = False |             gw_state_new: bool = False | ||||||
|             device_new: dict = gw_list.pop(did, None) |             device_new = gw_list.pop(did, None) | ||||||
|             if device_new: |             if device_new: | ||||||
|                 # Update gateway device info |                 # Update gateway device info | ||||||
|                 self._device_list_gateway[did] = { |                 self._device_list_gateway[did] = { | ||||||
| @@ -1493,7 +1497,7 @@ class MIoTClient: | |||||||
|             if state_old == state_new: |             if state_old == state_new: | ||||||
|                 continue |                 continue | ||||||
|             info['online'] = state_new |             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: |             if sub and sub.handler: | ||||||
|                 sub.handler( |                 sub.handler( | ||||||
|                     did, MIoTDeviceState.ONLINE if state_new |                     did, MIoTDeviceState.ONLINE if state_new | ||||||
| @@ -1518,7 +1522,7 @@ class MIoTClient: | |||||||
|             if state_old == state_new: |             if state_old == state_new: | ||||||
|                 continue |                 continue | ||||||
|             self._device_list_cache[did]['online'] = state_new |             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: |             if sub and sub.handler: | ||||||
|                 sub.handler( |                 sub.handler( | ||||||
|                     did, MIoTDeviceState.ONLINE if state_new |                     did, MIoTDeviceState.ONLINE if state_new | ||||||
| @@ -1533,7 +1537,7 @@ class MIoTClient: | |||||||
|             'refresh gw devices with group_id, %s', group_id) |             'refresh gw devices with group_id, %s', group_id) | ||||||
|         # Remove timer |         # Remove timer | ||||||
|         self._mips_local_state_changed_timers.pop(group_id, None) |         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: |         if not mips: | ||||||
|             _LOGGER.error('mips not exist, %s', group_id) |             _LOGGER.error('mips not exist, %s', group_id) | ||||||
|             return |             return | ||||||
| @@ -1900,77 +1904,73 @@ async def get_miot_instance_async( | |||||||
| ) -> MIoTClient: | ) -> MIoTClient: | ||||||
|     if entry_id is None: |     if entry_id is None: | ||||||
|         raise MIoTClientError('invalid entry_id') |         raise MIoTClientError('invalid entry_id') | ||||||
|     miot_client: MIoTClient = None |     miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None) | ||||||
|     if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None): |     if miot_client: | ||||||
|         _LOGGER.info('instance exist, %s', entry_id) |         _LOGGER.info('instance exist, %s', entry_id) | ||||||
|         miot_client = a |         return miot_client | ||||||
|     else: |     # Create new instance | ||||||
|         if entry_data is None: |     if not entry_data: | ||||||
|             raise MIoTClientError('entry data is None') |         raise MIoTClientError('entry data is None') | ||||||
|         # Get running loop |     # Get running loop | ||||||
|         loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() |     loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() | ||||||
|         if loop is None: |     if not loop: | ||||||
|             raise MIoTClientError('loop is None') |         raise MIoTClientError('loop is None') | ||||||
|         # MIoT storage |     # MIoT storage | ||||||
|         storage: Optional[MIoTStorage] = hass.data[DOMAIN].get( |     storage: Optional[MIoTStorage] = hass.data[DOMAIN].get( | ||||||
|             'miot_storage', None) |         'miot_storage', None) | ||||||
|         if not storage: |     if not storage: | ||||||
|             storage = MIoTStorage( |         storage = MIoTStorage( | ||||||
|                 root_path=entry_data['storage_path'], loop=loop) |             root_path=entry_data['storage_path'], loop=loop) | ||||||
|             hass.data[DOMAIN]['miot_storage'] = storage |         hass.data[DOMAIN]['miot_storage'] = storage | ||||||
|             _LOGGER.info('create miot_storage instance') |         _LOGGER.info('create miot_storage instance') | ||||||
|         global_config: dict = await storage.load_user_config_async( |     global_config: dict = await storage.load_user_config_async( | ||||||
|             uid='global_config', cloud_server='all', |         uid='global_config', cloud_server='all', | ||||||
|             keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe']) |         keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe']) | ||||||
|         # MIoT network |     # MIoT network | ||||||
|         network_detect_addr: dict = global_config.get( |     network_detect_addr: dict = global_config.get('network_detect_addr', {}) | ||||||
|             'network_detect_addr', {}) |     network: Optional[MIoTNetwork] = hass.data[DOMAIN].get( | ||||||
|         network: Optional[MIoTNetwork] = hass.data[DOMAIN].get( |         'miot_network', None) | ||||||
|             'miot_network', None) |     if not network: | ||||||
|         if not network: |         network = MIoTNetwork( | ||||||
|             network = MIoTNetwork( |             ip_addr_list=network_detect_addr.get('ip', []), | ||||||
|                 ip_addr_list=network_detect_addr.get('ip', []), |             url_addr_list=network_detect_addr.get('url', []), | ||||||
|                 url_addr_list=network_detect_addr.get('url', []), |             refresh_interval=NETWORK_REFRESH_INTERVAL, | ||||||
|                 refresh_interval=NETWORK_REFRESH_INTERVAL, |             loop=loop) | ||||||
|                 loop=loop) |         hass.data[DOMAIN]['miot_network'] = network | ||||||
|             hass.data[DOMAIN]['miot_network'] = network |         await network.init_async() | ||||||
|             await network.init_async() |         _LOGGER.info('create miot_network instance') | ||||||
|             _LOGGER.info('create miot_network instance') |     # MIoT service | ||||||
|         # MIoT service |     mips_service: Optional[MipsService] = hass.data[DOMAIN].get( | ||||||
|         mips_service: Optional[MipsService] = hass.data[DOMAIN].get( |         'mips_service', None) | ||||||
|             'mips_service', None) |     if not mips_service: | ||||||
|         if not mips_service: |         aiozc = await zeroconf.async_get_async_instance(hass) | ||||||
|             aiozc = await zeroconf.async_get_async_instance(hass) |         mips_service = MipsService(aiozc=aiozc, loop=loop) | ||||||
|             mips_service = MipsService(aiozc=aiozc, loop=loop) |         hass.data[DOMAIN]['mips_service'] = mips_service | ||||||
|             hass.data[DOMAIN]['mips_service'] = mips_service |         await mips_service.init_async() | ||||||
|             await mips_service.init_async() |         _LOGGER.info('create mips_service instance') | ||||||
|             _LOGGER.info('create mips_service instance') |     # MIoT lan | ||||||
|         # MIoT lan |     miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None) | ||||||
|         miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get( |     if not miot_lan: | ||||||
|             'miot_lan', None) |         miot_lan = MIoTLan( | ||||||
|         if not miot_lan: |             net_ifs=global_config.get('net_interfaces', []), | ||||||
|             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, |  | ||||||
|             network=network, |             network=network, | ||||||
|             storage=storage, |  | ||||||
|             mips_service=mips_service, |             mips_service=mips_service, | ||||||
|             miot_lan=miot_lan, |             enable_subscribe=global_config.get('enable_subscribe', False), | ||||||
|             loop=loop |             loop=loop) | ||||||
|         ) |         hass.data[DOMAIN]['miot_lan'] = miot_lan | ||||||
|         miot_client.persistent_notify = persistent_notify |         _LOGGER.info('create miot_lan instance') | ||||||
|         hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client) |     # MIoT client | ||||||
|         _LOGGER.info( |     miot_client = MIoTClient( | ||||||
|             'new miot_client instance, %s, %s', entry_id, entry_data) |         entry_id=entry_id, | ||||||
|         await miot_client.init_async() |         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 |     return miot_client | ||||||
|   | |||||||
| @@ -382,7 +382,7 @@ class MIoTHttpClient: | |||||||
|  |  | ||||||
|         return res_obj['data'] |         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): |         if not isinstance(csr, str): | ||||||
|             raise MIoTHttpError('invalid params') |             raise MIoTHttpError('invalid params') | ||||||
|  |  | ||||||
|   | |||||||
| @@ -56,6 +56,7 @@ from homeassistant.const import ( | |||||||
|     CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, |     CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, | ||||||
|     CONCENTRATION_PARTS_PER_BILLION, |     CONCENTRATION_PARTS_PER_BILLION, | ||||||
|     CONCENTRATION_PARTS_PER_MILLION, |     CONCENTRATION_PARTS_PER_MILLION, | ||||||
|  |     DEGREE, | ||||||
|     LIGHT_LUX, |     LIGHT_LUX, | ||||||
|     PERCENTAGE, |     PERCENTAGE, | ||||||
|     SIGNAL_STRENGTH_DECIBELS, |     SIGNAL_STRENGTH_DECIBELS, | ||||||
| @@ -72,6 +73,7 @@ from homeassistant.const import ( | |||||||
|     UnitOfPower, |     UnitOfPower, | ||||||
|     UnitOfVolume, |     UnitOfVolume, | ||||||
|     UnitOfVolumeFlowRate, |     UnitOfVolumeFlowRate, | ||||||
|  |     UnitOfDataRate | ||||||
| ) | ) | ||||||
| from homeassistant.helpers.entity import DeviceInfo | from homeassistant.helpers.entity import DeviceInfo | ||||||
| from homeassistant.components.switch import SwitchDeviceClass | from homeassistant.components.switch import SwitchDeviceClass | ||||||
| @@ -243,12 +245,12 @@ class MIoTDevice: | |||||||
|     def sub_device_state( |     def sub_device_state( | ||||||
|         self, key: str, handler: Callable[[str, MIoTDeviceState], None] |         self, key: str, handler: Callable[[str, MIoTDeviceState], None] | ||||||
|     ) -> int: |     ) -> int: | ||||||
|         self._sub_id += 1 |         sub_id = self.__gen_sub_id() | ||||||
|         if key in self._device_state_sub_list: |         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: |         else: | ||||||
|             self._device_state_sub_list[key] = {str(self._sub_id): handler} |             self._device_state_sub_list[key] = {str(sub_id): handler} | ||||||
|         return self._sub_id |         return sub_id | ||||||
|  |  | ||||||
|     def unsub_device_state(self, key: str, sub_id: int) -> None: |     def unsub_device_state(self, key: str, sub_id: int) -> None: | ||||||
|         sub_list = self._device_state_sub_list.get(key, 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(): |             for handler in self._value_sub_list[key].values(): | ||||||
|                 handler(params, ctx) |                 handler(params, ctx) | ||||||
|  |  | ||||||
|         self._sub_id += 1 |         sub_id = self.__gen_sub_id() | ||||||
|         if key in self._value_sub_list: |         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: |         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( |             self.miot_client.sub_prop( | ||||||
|                 did=self._did, handler=_on_prop_changed, siid=siid, piid=piid) |                 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: |     def unsub_property(self, siid: int, piid: int, sub_id: int) -> None: | ||||||
|         key: str = f'p.{siid}.{piid}' |         key: str = f'p.{siid}.{piid}' | ||||||
| @@ -294,14 +296,14 @@ class MIoTDevice: | |||||||
|             for handler in self._value_sub_list[key].values(): |             for handler in self._value_sub_list[key].values(): | ||||||
|                 handler(params, ctx) |                 handler(params, ctx) | ||||||
|  |  | ||||||
|         self._sub_id += 1 |         sub_id = self.__gen_sub_id() | ||||||
|         if key in self._value_sub_list: |         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: |         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( |             self.miot_client.sub_event( | ||||||
|                 did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid) |                 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: |     def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None: | ||||||
|         key: str = f'e.{siid}.{eiid}' |         key: str = f'e.{siid}.{eiid}' | ||||||
| @@ -414,10 +416,12 @@ class MIoTDevice: | |||||||
|         spec_name: str = spec_instance.name |         spec_name: str = spec_instance.name | ||||||
|         if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str): |         if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str): | ||||||
|             spec_name = SPEC_DEVICE_TRANS_MAP[spec_name] |             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. |         # 1. The device shall have all required services. | ||||||
|         required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys() |         required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys() | ||||||
|         if not { |         if not { | ||||||
|                 service.name for service in spec_instance.services |             service.name for service in spec_instance.services | ||||||
|         }.issuperset(required_services): |         }.issuperset(required_services): | ||||||
|             return None |             return None | ||||||
|         optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys() |         optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys() | ||||||
| @@ -427,9 +431,13 @@ class MIoTDevice: | |||||||
|         for service in spec_instance.services: |         for service in spec_instance.services: | ||||||
|             if service.platform: |             if service.platform: | ||||||
|                 continue |                 continue | ||||||
|  |             required_properties: dict | ||||||
|  |             optional_properties: dict | ||||||
|  |             required_actions: set | ||||||
|  |             optional_actions: set | ||||||
|             # 2. The service shall have all required properties, actions. |             # 2. The service shall have all required properties, actions. | ||||||
|             if service.name in required_services: |             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( |                     'required'].get( | ||||||
|                         service.name, {} |                         service.name, {} | ||||||
|                 ).get('required', {}).get('properties', {}) |                 ).get('required', {}).get('properties', {}) | ||||||
| @@ -446,7 +454,7 @@ class MIoTDevice: | |||||||
|                         service.name, {} |                         service.name, {} | ||||||
|                 ).get('optional', {}).get('actions', set({})) |                 ).get('optional', {}).get('actions', set({})) | ||||||
|             elif service.name in optional_services: |             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( |                     'optional'].get( | ||||||
|                         service.name, {} |                         service.name, {} | ||||||
|                 ).get('required', {}).get('properties', {}) |                 ).get('required', {}).get('properties', {}) | ||||||
| @@ -484,7 +492,7 @@ class MIoTDevice: | |||||||
|                         set(required_properties.keys()), optional_properties): |                         set(required_properties.keys()), optional_properties): | ||||||
|                     if prop.unit: |                     if prop.unit: | ||||||
|                         prop.external_unit = self.unit_convert(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 |                     prop.platform = platform | ||||||
|                     entity_data.props.add(prop) |                     entity_data.props.add(prop) | ||||||
|             # action |             # action | ||||||
| @@ -499,85 +507,95 @@ class MIoTDevice: | |||||||
|         return entity_data |         return entity_data | ||||||
|  |  | ||||||
|     def parse_miot_service_entity( |     def parse_miot_service_entity( | ||||||
|         self, service_instance: MIoTSpecService |         self, miot_service: MIoTSpecService | ||||||
|     ) -> Optional[MIoTEntityData]: |     ) -> Optional[MIoTEntityData]: | ||||||
|         service = service_instance |         if ( | ||||||
|         if service.platform or (service.name not in SPEC_SERVICE_TRANS_MAP): |             miot_service.platform | ||||||
|  |             or miot_service.name not in SPEC_SERVICE_TRANS_MAP | ||||||
|  |         ): | ||||||
|             return None |             return None | ||||||
|  |         service_name = miot_service.name | ||||||
|         service_name = service.name |  | ||||||
|         if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str): |         if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str): | ||||||
|             service_name = SPEC_SERVICE_TRANS_MAP[service_name] |             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_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][ | ||||||
|             'required'].get('properties', {}) |             'required'].get('properties', {}) | ||||||
|         if not { |         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())): |         }.issuperset(set(required_properties.keys())): | ||||||
|             return None |             return None | ||||||
|         # 2. The required property shall have all required access mode. |         for prop in miot_service.properties: | ||||||
|         for prop in service.properties: |  | ||||||
|             if prop.name in required_properties: |             if prop.name in required_properties: | ||||||
|                 if not set(prop.access).issuperset( |                 if not set(prop.access).issuperset( | ||||||
|                         required_properties[prop.name]): |                         required_properties[prop.name]): | ||||||
|                     return None |                     return None | ||||||
|  |         # Required actions | ||||||
|  |         # Required events | ||||||
|         platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity'] |         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_properties = SPEC_SERVICE_TRANS_MAP[service_name][ | ||||||
|             'optional'].get('properties', set({})) |             'optional'].get('properties', set({})) | ||||||
|         for prop in service.properties: |         for prop in miot_service.properties: | ||||||
|             if prop.name in set.union( |             if prop.name in set.union( | ||||||
|                     set(required_properties.keys()), optional_properties): |                     set(required_properties.keys()), optional_properties): | ||||||
|                 if prop.unit: |                 if prop.unit: | ||||||
|                     prop.external_unit = self.unit_convert(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 |                 prop.platform = platform | ||||||
|                 entity_data.props.add(prop) |                 entity_data.props.add(prop) | ||||||
|         # action |         # Optional actions | ||||||
|         # event |         # Optional events | ||||||
|         # No actions or events is in SPEC_SERVICE_TRANS_MAP now. |         miot_service.platform = platform | ||||||
|         service.platform = platform |  | ||||||
|         return entity_data |         return entity_data | ||||||
|  |  | ||||||
|     def parse_miot_property_entity( |     def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool: | ||||||
|         self, property_instance: MIoTSpecProperty |  | ||||||
|     ) -> Optional[dict[str, str]]: |  | ||||||
|         prop = property_instance |  | ||||||
|         if ( |         if ( | ||||||
|             prop.platform |             miot_prop.platform | ||||||
|             or (prop.name not in SPEC_PROP_TRANS_MAP['properties']) |             or miot_prop.name not in SPEC_PROP_TRANS_MAP['properties'] | ||||||
|         ): |         ): | ||||||
|             return None |             return False | ||||||
|  |         prop_name = miot_prop.name | ||||||
|         prop_name = prop.name |  | ||||||
|         if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str): |         if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str): | ||||||
|             prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name] |             prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name] | ||||||
|         platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity'] |         platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity'] | ||||||
|  |         # Check | ||||||
|         prop_access: set = set({}) |         prop_access: set = set({}) | ||||||
|         if prop.readable: |         if miot_prop.readable: | ||||||
|             prop_access.add('read') |             prop_access.add('read') | ||||||
|         if prop.writable: |         if miot_prop.writable: | ||||||
|             prop_access.add('write') |             prop_access.add('write') | ||||||
|         if prop_access != (SPEC_PROP_TRANS_MAP[ |         if prop_access != (SPEC_PROP_TRANS_MAP[ | ||||||
|                 'entities'][platform]['access']): |                 'entities'][platform]['access']): | ||||||
|             return None |             return False | ||||||
|         if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[ |         if miot_prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[ | ||||||
|                 'entities'][platform]['format']: |                 'entities'][platform]['format']: | ||||||
|             return None |             return False | ||||||
|         if prop.unit: |         miot_prop.device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][ | ||||||
|             prop.external_unit = self.unit_convert(prop.unit) |  | ||||||
|             prop.icon = self.icon_convert(prop.unit) |  | ||||||
|         device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][ |  | ||||||
|             'device_class'] |             'device_class'] | ||||||
|         result = {'platform': platform, 'device_class': device_class} |         # Optional params | ||||||
|         # optional: |         if 'state_class' in SPEC_PROP_TRANS_MAP['properties'][prop_name]: | ||||||
|         if 'optional' in SPEC_PROP_TRANS_MAP['properties'][prop_name]: |             miot_prop.state_class = SPEC_PROP_TRANS_MAP['properties'][ | ||||||
|             optional = SPEC_PROP_TRANS_MAP['properties'][prop_name]['optional'] |                 prop_name]['state_class'] | ||||||
|             if 'state_class' in optional: |         if ( | ||||||
|                 result['state_class'] = optional['state_class'] |             not miot_prop.external_unit | ||||||
|             if not prop.unit and 'unit_of_measurement' in optional: |             and 'unit_of_measurement' in SPEC_PROP_TRANS_MAP['properties'][ | ||||||
|                 result['unit_of_measurement'] = optional['unit_of_measurement'] |                 prop_name] | ||||||
|         return result |         ): | ||||||
|  |             # 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: |     def spec_transform(self) -> None: | ||||||
|         """Parse service, property, event, action from device spec.""" |         """Parse service, property, event, action from device spec.""" | ||||||
| @@ -589,7 +607,7 @@ class MIoTDevice: | |||||||
|         # STEP 2: service conversion |         # STEP 2: service conversion | ||||||
|         for service in self.spec_instance.services: |         for service in self.spec_instance.services: | ||||||
|             service_entity = self.parse_miot_service_entity( |             service_entity = self.parse_miot_service_entity( | ||||||
|                 service_instance=service) |                 miot_service=service) | ||||||
|             if service_entity: |             if service_entity: | ||||||
|                 self.append_entity(entity_data=service_entity) |                 self.append_entity(entity_data=service_entity) | ||||||
|             # STEP 3.1: property conversion |             # STEP 3.1: property conversion | ||||||
| @@ -598,20 +616,11 @@ class MIoTDevice: | |||||||
|                     continue |                     continue | ||||||
|                 if prop.unit: |                 if prop.unit: | ||||||
|                     prop.external_unit = self.unit_convert(prop.unit) |                     prop.external_unit = self.unit_convert(prop.unit) | ||||||
|                     prop.icon = self.icon_convert(prop.unit) |                     if not prop.icon: | ||||||
|                 prop_entity = self.parse_miot_property_entity( |                         prop.icon = self.icon_convert(prop.unit) | ||||||
|                     property_instance=prop) |                 # Special conversion | ||||||
|                 if prop_entity: |                 self.parse_miot_property_entity(miot_prop=prop) | ||||||
|                     prop.platform = prop_entity['platform'] |                 # General conversion | ||||||
|                     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.platform: |                 if not prop.platform: | ||||||
|                     if prop.writable: |                     if prop.writable: | ||||||
|                         if prop.format_ == str: |                         if prop.format_ == str: | ||||||
| @@ -625,7 +634,7 @@ class MIoTDevice: | |||||||
|                             prop.platform = 'number' |                             prop.platform = 'number' | ||||||
|                         else: |                         else: | ||||||
|                             # Irregular property will not be transformed. |                             # Irregular property will not be transformed. | ||||||
|                             pass |                             continue | ||||||
|                     elif prop.readable or prop.notifiable: |                     elif prop.readable or prop.notifiable: | ||||||
|                         if prop.format_ == bool: |                         if prop.format_ == bool: | ||||||
|                             prop.platform = 'binary_sensor' |                             prop.platform = 'binary_sensor' | ||||||
| @@ -653,11 +662,66 @@ class MIoTDevice: | |||||||
|                 self.append_action(action=action) |                 self.append_action(action=action) | ||||||
|  |  | ||||||
|     def unit_convert(self, spec_unit: str) -> Optional[str]: |     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 = { |         unit_map = { | ||||||
|             'percentage': PERCENTAGE, |             'percentage': PERCENTAGE, | ||||||
|             'weeks': UnitOfTime.WEEKS, |             'weeks': UnitOfTime.WEEKS, | ||||||
|             'days': UnitOfTime.DAYS, |             'days': UnitOfTime.DAYS, | ||||||
|  |             'hour': UnitOfTime.HOURS, | ||||||
|             'hours': UnitOfTime.HOURS, |             'hours': UnitOfTime.HOURS, | ||||||
|             'minutes': UnitOfTime.MINUTES, |             'minutes': UnitOfTime.MINUTES, | ||||||
|             'seconds': UnitOfTime.SECONDS, |             'seconds': UnitOfTime.SECONDS, | ||||||
| @@ -672,30 +736,48 @@ class MIoTDevice: | |||||||
|             'ppb': CONCENTRATION_PARTS_PER_BILLION, |             'ppb': CONCENTRATION_PARTS_PER_BILLION, | ||||||
|             'lux': LIGHT_LUX, |             'lux': LIGHT_LUX, | ||||||
|             'pascal': UnitOfPressure.PA, |             'pascal': UnitOfPressure.PA, | ||||||
|  |             'kilopascal': UnitOfPressure.KPA, | ||||||
|  |             'mmHg': UnitOfPressure.MMHG, | ||||||
|             'bar': UnitOfPressure.BAR, |             'bar': UnitOfPressure.BAR, | ||||||
|             'watt': UnitOfPower.WATT, |  | ||||||
|             'L': UnitOfVolume.LITERS, |             'L': UnitOfVolume.LITERS, | ||||||
|  |             'liter': UnitOfVolume.LITERS, | ||||||
|             'mL': UnitOfVolume.MILLILITERS, |             'mL': UnitOfVolume.MILLILITERS, | ||||||
|             'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR, |             'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR, | ||||||
|             'm/s': UnitOfSpeed.METERS_PER_SECOND, |             'm/s': UnitOfSpeed.METERS_PER_SECOND, | ||||||
|  |             'watt': UnitOfPower.WATT, | ||||||
|  |             'w': UnitOfPower.WATT, | ||||||
|  |             'W': UnitOfPower.WATT, | ||||||
|             'kWh': UnitOfEnergy.KILO_WATT_HOUR, |             'kWh': UnitOfEnergy.KILO_WATT_HOUR, | ||||||
|             'A': UnitOfElectricCurrent.AMPERE, |             'A': UnitOfElectricCurrent.AMPERE, | ||||||
|             'mA': UnitOfElectricCurrent.MILLIAMPERE, |             'mA': UnitOfElectricCurrent.MILLIAMPERE, | ||||||
|             'V': UnitOfElectricPotential.VOLT, |             'V': UnitOfElectricPotential.VOLT, | ||||||
|  |             'mv': UnitOfElectricPotential.MILLIVOLT, | ||||||
|             'mV': UnitOfElectricPotential.MILLIVOLT, |             'mV': UnitOfElectricPotential.MILLIVOLT, | ||||||
|  |             'cm': UnitOfLength.CENTIMETERS, | ||||||
|             'm': UnitOfLength.METERS, |             'm': UnitOfLength.METERS, | ||||||
|  |             'meter': UnitOfLength.METERS, | ||||||
|             'km': UnitOfLength.KILOMETERS, |             'km': UnitOfLength.KILOMETERS, | ||||||
|             'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, |             'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, | ||||||
|             'gram': UnitOfMass.GRAMS, |             'gram': UnitOfMass.GRAMS, | ||||||
|  |             'kilogram': UnitOfMass.KILOGRAMS, | ||||||
|             'dB': SIGNAL_STRENGTH_DECIBELS, |             'dB': SIGNAL_STRENGTH_DECIBELS, | ||||||
|  |             'arcdegrees': DEGREE, | ||||||
|  |             'arcdegress': DEGREE, | ||||||
|             'kB': UnitOfInformation.KILOBYTES, |             '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 |         # Handle UnitOfConductivity separately since | ||||||
|         # it might not be available in all HA versions |         # it might not be available in all HA versions | ||||||
|         try: |         try: | ||||||
|             # pylint: disable=import-outside-toplevel |             # 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 |             unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM | ||||||
|         except Exception:  # pylint: disable=broad-except |         except Exception:  # pylint: disable=broad-except | ||||||
|             unit_map['μS/cm'] = 'μS/cm' |             unit_map['μS/cm'] = 'μS/cm' | ||||||
| @@ -703,59 +785,66 @@ class MIoTDevice: | |||||||
|         return unit_map.get(spec_unit, None) |         return unit_map.get(spec_unit, None) | ||||||
|  |  | ||||||
|     def icon_convert(self, spec_unit: str) -> Optional[str]: |     def icon_convert(self, spec_unit: str) -> Optional[str]: | ||||||
|         if spec_unit in ['percentage']: |         if spec_unit in {'percentage'}: | ||||||
|             return 'mdi:percent' |             return 'mdi:percent' | ||||||
|         if spec_unit in [ |         if spec_unit in { | ||||||
|                 'weeks', 'days', 'hours', 'minutes', 'seconds', 'ms', 'μs']: |             'weeks', 'days', 'hour', 'hours', 'minutes', 'seconds', 'ms', 'μs' | ||||||
|  |         }: | ||||||
|             return 'mdi:clock' |             return 'mdi:clock' | ||||||
|         if spec_unit in ['celsius']: |         if spec_unit in {'celsius'}: | ||||||
|             return 'mdi:temperature-celsius' |             return 'mdi:temperature-celsius' | ||||||
|         if spec_unit in ['fahrenheit']: |         if spec_unit in {'fahrenheit'}: | ||||||
|             return 'mdi:temperature-fahrenheit' |             return 'mdi:temperature-fahrenheit' | ||||||
|         if spec_unit in ['kelvin']: |         if spec_unit in {'kelvin'}: | ||||||
|             return 'mdi:temperature-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' |             return 'mdi:blur' | ||||||
|         if spec_unit in ['lux']: |         if spec_unit in {'lux'}: | ||||||
|             return 'mdi:brightness-6' |             return 'mdi:brightness-6' | ||||||
|         if spec_unit in ['pascal', 'megapascal', 'bar']: |         if spec_unit in {'pascal', 'kilopascal', 'megapascal', 'mmHg', 'bar'}: | ||||||
|             return 'mdi:gauge' |             return 'mdi:gauge' | ||||||
|         if spec_unit in ['watt']: |         if spec_unit in {'watt', 'w', 'W'}: | ||||||
|             return 'mdi:flash-triangle' |             return 'mdi:flash-triangle' | ||||||
|         if spec_unit in ['L', 'mL']: |         if spec_unit in {'L', 'mL'}: | ||||||
|             return 'mdi:gas-cylinder' |             return 'mdi:gas-cylinder' | ||||||
|         if spec_unit in ['km/h', 'm/s']: |         if spec_unit in {'km/h', 'm/s'}: | ||||||
|             return 'mdi:speedometer' |             return 'mdi:speedometer' | ||||||
|         if spec_unit in ['kWh']: |         if spec_unit in {'kWh'}: | ||||||
|             return 'mdi:transmission-tower' |             return 'mdi:transmission-tower' | ||||||
|         if spec_unit in ['A', 'mA']: |         if spec_unit in {'A', 'mA'}: | ||||||
|             return 'mdi:current-ac' |             return 'mdi:current-ac' | ||||||
|         if spec_unit in ['V', 'mV']: |         if spec_unit in {'V', 'mv', 'mV'}: | ||||||
|             return 'mdi:current-dc' |             return 'mdi:current-dc' | ||||||
|         if spec_unit in ['m', 'km']: |         if spec_unit in {'cm', 'm', 'meter', 'km'}: | ||||||
|             return 'mdi:ruler' |             return 'mdi:ruler' | ||||||
|         if spec_unit in ['rgb']: |         if spec_unit in {'rgb'}: | ||||||
|             return 'mdi:palette' |             return 'mdi:palette' | ||||||
|         if spec_unit in ['m3/h', 'L/s']: |         if spec_unit in {'m3/h', 'L/s'}: | ||||||
|             return 'mdi:pipe-leak' |             return 'mdi:pipe-leak' | ||||||
|         if spec_unit in ['μS/cm']: |         if spec_unit in {'μS/cm'}: | ||||||
|             return 'mdi:resistor-nodes' |             return 'mdi:resistor-nodes' | ||||||
|         if spec_unit in ['gram']: |         if spec_unit in {'gram', 'kilogram'}: | ||||||
|             return 'mdi:weight' |             return 'mdi:weight' | ||||||
|         if spec_unit in ['dB']: |         if spec_unit in {'dB'}: | ||||||
|             return 'mdi:signal-distance-variant' |             return 'mdi:signal-distance-variant' | ||||||
|         if spec_unit in ['times']: |         if spec_unit in {'times'}: | ||||||
|             return 'mdi:counter' |             return 'mdi:counter' | ||||||
|         if spec_unit in ['mmol/L']: |         if spec_unit in {'mmol/L'}: | ||||||
|             return 'mdi:dots-hexagon' |             return 'mdi:dots-hexagon' | ||||||
|         if spec_unit in ['arcdegress']: |         if spec_unit in {'kB', 'MB', 'GB'}: | ||||||
|             return 'mdi:angle-obtuse' |  | ||||||
|         if spec_unit in ['kB']: |  | ||||||
|             return 'mdi:network-pos' |             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 'mdi:food' | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|  |     def __gen_sub_id(self) -> int: | ||||||
|  |         self._sub_id += 1 | ||||||
|  |         return self._sub_id | ||||||
|  |  | ||||||
|     def __on_device_state_changed( |     def __on_device_state_changed( | ||||||
|         self, did: str, state: MIoTDeviceState, ctx: Any |         self, did: str, state: MIoTDeviceState, ctx: Any | ||||||
|     ) -> None: |     ) -> None: | ||||||
| @@ -1184,6 +1273,7 @@ class MIoTPropertyEntity(Entity): | |||||||
|     def __on_value_changed(self, params: dict, ctx: Any) -> None: |     def __on_value_changed(self, params: dict, ctx: Any) -> None: | ||||||
|         _LOGGER.debug('property changed, %s', params) |         _LOGGER.debug('property changed, %s', params) | ||||||
|         self._value = self.spec.value_format(params['value']) |         self._value = self.spec.value_format(params['value']) | ||||||
|  |         self._value = self.spec.eval_expr(self._value) | ||||||
|         if not self._pending_write_ha_state_timer: |         if not self._pending_write_ha_state_timer: | ||||||
|             self.async_write_ha_state() |             self.async_write_ha_state() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1414,7 +1414,7 @@ class MipsLocalClient(_MipsClient): | |||||||
|     @final |     @final | ||||||
|     @on_dev_list_changed.setter |     @on_dev_list_changed.setter | ||||||
|     def on_dev_list_changed( |     def on_dev_list_changed( | ||||||
|         self, func: Callable[[Any, list[str]], Coroutine] |         self, func: Optional[Callable[[Any, list[str]], Coroutine]] | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """run in main loop.""" |         """run in main loop.""" | ||||||
|         self._on_dev_list_changed = func |         self._on_dev_list_changed = func | ||||||
|   | |||||||
| @@ -469,14 +469,13 @@ class _MIoTSpecBase: | |||||||
|     proprietary: bool |     proprietary: bool | ||||||
|     need_filter: bool |     need_filter: bool | ||||||
|     name: str |     name: str | ||||||
|  |     icon: Optional[str] | ||||||
|  |  | ||||||
|     # External params |     # External params | ||||||
|     platform: Optional[str] |     platform: Optional[str] | ||||||
|     device_class: Any |     device_class: Any | ||||||
|     state_class: Any |     state_class: Any | ||||||
|     icon: Optional[str] |  | ||||||
|     external_unit: Any |     external_unit: Any | ||||||
|     expression: Optional[str] |  | ||||||
|  |  | ||||||
|     spec_id: int |     spec_id: int | ||||||
|  |  | ||||||
| @@ -489,13 +488,12 @@ class _MIoTSpecBase: | |||||||
|         self.proprietary = spec.get('proprietary', False) |         self.proprietary = spec.get('proprietary', False) | ||||||
|         self.need_filter = spec.get('need_filter', False) |         self.need_filter = spec.get('need_filter', False) | ||||||
|         self.name = spec.get('name', 'xiaomi') |         self.name = spec.get('name', 'xiaomi') | ||||||
|  |         self.icon = spec.get('icon', None) | ||||||
|  |  | ||||||
|         self.platform = None |         self.platform = None | ||||||
|         self.device_class = None |         self.device_class = None | ||||||
|         self.state_class = None |         self.state_class = None | ||||||
|         self.icon = None |  | ||||||
|         self.external_unit = None |         self.external_unit = None | ||||||
|         self.expression = None |  | ||||||
|  |  | ||||||
|         self.spec_id = hash(f'{self.type_}.{self.iid}') |         self.spec_id = hash(f'{self.type_}.{self.iid}') | ||||||
|  |  | ||||||
| @@ -510,6 +508,7 @@ class MIoTSpecProperty(_MIoTSpecBase): | |||||||
|     """MIoT SPEC property class.""" |     """MIoT SPEC property class.""" | ||||||
|     unit: Optional[str] |     unit: Optional[str] | ||||||
|     precision: int |     precision: int | ||||||
|  |     expr: Optional[str] | ||||||
|  |  | ||||||
|     _format_: Type |     _format_: Type | ||||||
|     _value_range: Optional[MIoTSpecValueRange] |     _value_range: Optional[MIoTSpecValueRange] | ||||||
| @@ -531,7 +530,8 @@ class MIoTSpecProperty(_MIoTSpecBase): | |||||||
|             unit: Optional[str] = None, |             unit: Optional[str] = None, | ||||||
|             value_range: Optional[dict] = None, |             value_range: Optional[dict] = None, | ||||||
|             value_list: Optional[list[dict]] = None, |             value_list: Optional[list[dict]] = None, | ||||||
|             precision: Optional[int] = None |             precision: Optional[int] = None, | ||||||
|  |             expr: Optional[str] = None | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         super().__init__(spec=spec) |         super().__init__(spec=spec) | ||||||
|         self.service = service |         self.service = service | ||||||
| @@ -541,6 +541,7 @@ class MIoTSpecProperty(_MIoTSpecBase): | |||||||
|         self.value_range = value_range |         self.value_range = value_range | ||||||
|         self.value_list = value_list |         self.value_list = value_list | ||||||
|         self.precision = precision or 1 |         self.precision = precision or 1 | ||||||
|  |         self.expr = expr | ||||||
|  |  | ||||||
|         self.spec_id = hash( |         self.spec_id = hash( | ||||||
|             f'p.{self.name}.{self.service.iid}.{self.iid}') |             f'p.{self.name}.{self.service.iid}.{self.iid}') | ||||||
| @@ -613,6 +614,18 @@ class MIoTSpecProperty(_MIoTSpecBase): | |||||||
|         elif isinstance(value, MIoTSpecValueList): |         elif isinstance(value, MIoTSpecValueList): | ||||||
|             self._value_list = value |             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: |     def value_format(self, value: Any) -> Any: | ||||||
|         if value is None: |         if value is None: | ||||||
|             return None |             return None | ||||||
| @@ -639,7 +652,9 @@ class MIoTSpecProperty(_MIoTSpecBase): | |||||||
|             'value_range': ( |             'value_range': ( | ||||||
|                 self._value_range.dump() if self._value_range else None), |                 self._value_range.dump() if self._value_range else None), | ||||||
|             'value_list': self._value_list.dump() if self._value_list 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: | class MIoTSpecInstance: | ||||||
|     """MIoT SPEC instance class.""" |     """MIoT SPEC instance class.""" | ||||||
|     urn: str |     urn: str | ||||||
| @@ -774,7 +788,8 @@ class MIoTSpecInstance: | |||||||
|                     unit=prop['unit'], |                     unit=prop['unit'], | ||||||
|                     value_range=prop['value_range'], |                     value_range=prop['value_range'], | ||||||
|                     value_list=prop['value_list'], |                     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) |                 spec_service.properties.append(spec_prop) | ||||||
|             for event in service['events']: |             for event in service['events']: | ||||||
|                 spec_event = MIoTSpecEvent( |                 spec_event = MIoTSpecEvent( | ||||||
| @@ -1119,6 +1134,79 @@ class _SpecFilter: | |||||||
|         return False |         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: | class MIoTSpecParser: | ||||||
|     """MIoT SPEC parser.""" |     """MIoT SPEC parser.""" | ||||||
|     # pylint: disable=inconsistent-quotes |     # pylint: disable=inconsistent-quotes | ||||||
| @@ -1132,6 +1220,7 @@ class MIoTSpecParser: | |||||||
|     _multi_lang: _MIoTSpecMultiLang |     _multi_lang: _MIoTSpecMultiLang | ||||||
|     _bool_trans: _SpecBoolTranslation |     _bool_trans: _SpecBoolTranslation | ||||||
|     _spec_filter: _SpecFilter |     _spec_filter: _SpecFilter | ||||||
|  |     _spec_modify: _SpecModify | ||||||
|  |  | ||||||
|     _init_done: bool |     _init_done: bool | ||||||
|  |  | ||||||
| @@ -1149,6 +1238,7 @@ class MIoTSpecParser: | |||||||
|         self._bool_trans = _SpecBoolTranslation( |         self._bool_trans = _SpecBoolTranslation( | ||||||
|             lang=self._lang, loop=self._main_loop) |             lang=self._lang, loop=self._main_loop) | ||||||
|         self._spec_filter = _SpecFilter(loop=self._main_loop) |         self._spec_filter = _SpecFilter(loop=self._main_loop) | ||||||
|  |         self._spec_modify = _SpecModify(loop=self._main_loop) | ||||||
|  |  | ||||||
|         self._init_done = False |         self._init_done = False | ||||||
|  |  | ||||||
| @@ -1157,6 +1247,7 @@ class MIoTSpecParser: | |||||||
|             return |             return | ||||||
|         await self._bool_trans.init_async() |         await self._bool_trans.init_async() | ||||||
|         await self._spec_filter.init_async() |         await self._spec_filter.init_async() | ||||||
|  |         await self._spec_modify.init_async() | ||||||
|         std_lib_cache = await self._storage.load_async( |         std_lib_cache = await self._storage.load_async( | ||||||
|             domain=self._DOMAIN, name='spec_std_lib', type_=dict) |             domain=self._DOMAIN, name='spec_std_lib', type_=dict) | ||||||
|         if ( |         if ( | ||||||
| @@ -1196,6 +1287,7 @@ class MIoTSpecParser: | |||||||
|         # self._std_lib.deinit() |         # self._std_lib.deinit() | ||||||
|         await self._bool_trans.deinit_async() |         await self._bool_trans.deinit_async() | ||||||
|         await self._spec_filter.deinit_async() |         await self._spec_filter.deinit_async() | ||||||
|  |         await self._spec_modify.deinit_async() | ||||||
|  |  | ||||||
|     async def parse( |     async def parse( | ||||||
|         self, urn: str, skip_cache: bool = False, |         self, urn: str, skip_cache: bool = False, | ||||||
| @@ -1275,6 +1367,8 @@ class MIoTSpecParser: | |||||||
|         await self._multi_lang.set_spec_async(urn=urn) |         await self._multi_lang.set_spec_async(urn=urn) | ||||||
|         # Set spec filter |         # Set spec filter | ||||||
|         await self._spec_filter.set_spec_spec(urn_key=urn_key) |         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 |         # Parse device type | ||||||
|         spec_instance: MIoTSpecInstance = MIoTSpecInstance( |         spec_instance: MIoTSpecInstance = MIoTSpecInstance( | ||||||
|             urn=urn, name=urn_strs[3], |             urn=urn, name=urn_strs[3], | ||||||
| @@ -1320,12 +1414,14 @@ class MIoTSpecParser: | |||||||
|                 ): |                 ): | ||||||
|                     continue |                     continue | ||||||
|                 p_type_strs: list[str] = property_['type'].split(':') |                 p_type_strs: list[str] = property_['type'].split(':') | ||||||
|  |                 # Handle special property.unit | ||||||
|  |                 unit = property_.get('unit', None) | ||||||
|                 spec_prop: MIoTSpecProperty = MIoTSpecProperty( |                 spec_prop: MIoTSpecProperty = MIoTSpecProperty( | ||||||
|                     spec=property_, |                     spec=property_, | ||||||
|                     service=spec_service, |                     service=spec_service, | ||||||
|                     format_=property_['format'], |                     format_=property_['format'], | ||||||
|                     access=property_['access'], |                     access=property_['access'], | ||||||
|                     unit=property_.get('unit', None)) |                     unit=unit if unit != 'none' else None) | ||||||
|                 spec_prop.name = p_type_strs[3] |                 spec_prop.name = p_type_strs[3] | ||||||
|                 # Filter spec property |                 # Filter spec property | ||||||
|                 spec_prop.need_filter = ( |                 spec_prop.need_filter = ( | ||||||
| @@ -1365,7 +1461,19 @@ class MIoTSpecParser: | |||||||
|                     if v_descriptions: |                     if v_descriptions: | ||||||
|                         # bool without value-list.name |                         # bool without value-list.name | ||||||
|                         spec_prop.value_list = v_descriptions |                         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) |                 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 |             # Parse service event | ||||||
|             for event in service.get('events', []): |             for event in service.get('events', []): | ||||||
|                 if ( |                 if ( | ||||||
|   | |||||||
| @@ -59,43 +59,6 @@ data: | |||||||
|   urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no |   urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no | ||||||
|   urn:miot-spec-v2:property:wind-reverse:00000117: yes_no |   urn:miot-spec-v2:property:wind-reverse:00000117: yes_no | ||||||
| translate: | 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: |   default: | ||||||
|     de: |     de: | ||||||
|       'false': Falsch |       'false': Falsch | ||||||
| @@ -133,6 +96,43 @@ translate: | |||||||
|     zh-Hant: |     zh-Hant: | ||||||
|       'false': 假 |       'false': 假 | ||||||
|       'true': 真 |       '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: |   motion_state: | ||||||
|     de: |     de: | ||||||
|       'false': Keine Bewegung erkannt |       '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.components.event import EventDeviceClass | ||||||
|  |  | ||||||
| from homeassistant.const import ( | from homeassistant.const import ( | ||||||
|  |     CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, | ||||||
|  |     LIGHT_LUX, | ||||||
|     UnitOfEnergy, |     UnitOfEnergy, | ||||||
|     UnitOfPower, |     UnitOfPower, | ||||||
|     UnitOfElectricCurrent, |     UnitOfElectricCurrent, | ||||||
|     UnitOfElectricPotential, |     UnitOfElectricPotential, | ||||||
|  |     UnitOfTemperature, | ||||||
|  |     UnitOfPressure, | ||||||
|  |     PERCENTAGE | ||||||
| ) | ) | ||||||
|  |  | ||||||
| # pylint: disable=pointless-string-statement | # 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': { |     'humidifier': { | ||||||
|         'required': { |         'required': { | ||||||
|             'humidifier': { |             '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': { |     'light': { | ||||||
|         'required': { |         'required': { | ||||||
|             'properties': { |             'properties': { | ||||||
| @@ -334,15 +339,13 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = { | |||||||
|         '<property instance name>':{ |         '<property instance name>':{ | ||||||
|             'device_class': str, |             'device_class': str, | ||||||
|             'entity': str, |             'entity': str, | ||||||
|             'optional':{ |             'state_class'?: str, | ||||||
|                 'state_class': str, |             'unit_of_measurement'?: str | ||||||
|                 'unit_of_measurement': str |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| """ | """ | ||||||
| SPEC_PROP_TRANS_MAP: dict[str, dict | str] = { | SPEC_PROP_TRANS_MAP: dict = { | ||||||
|     'entities': { |     'entities': { | ||||||
|         'sensor': { |         'sensor': { | ||||||
|             'format': {'int', 'float'}, |             'format': {'int', 'float'}, | ||||||
| @@ -356,107 +359,111 @@ SPEC_PROP_TRANS_MAP: dict[str, dict | str] = { | |||||||
|     'properties': { |     'properties': { | ||||||
|         'temperature': { |         'temperature': { | ||||||
|             'device_class': SensorDeviceClass.TEMPERATURE, |             'device_class': SensorDeviceClass.TEMPERATURE, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|  |             'unit_of_measurement': UnitOfTemperature.CELSIUS | ||||||
|         }, |         }, | ||||||
|         'relative-humidity': { |         'relative-humidity': { | ||||||
|             'device_class': SensorDeviceClass.HUMIDITY, |             'device_class': SensorDeviceClass.HUMIDITY, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|  |             'unit_of_measurement': PERCENTAGE | ||||||
|         }, |         }, | ||||||
|         'air-quality-index': { |         'air-quality-index': { | ||||||
|             'device_class': SensorDeviceClass.AQI, |             'device_class': SensorDeviceClass.AQI, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|         }, |         }, | ||||||
|         'pm2.5-density': { |         'pm2.5-density': { | ||||||
|             'device_class': SensorDeviceClass.PM25, |             'device_class': SensorDeviceClass.PM25, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|  |             'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER | ||||||
|         }, |         }, | ||||||
|         'pm10-density': { |         'pm10-density': { | ||||||
|             'device_class': SensorDeviceClass.PM10, |             'device_class': SensorDeviceClass.PM10, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|  |             'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER | ||||||
|         }, |         }, | ||||||
|         'pm1': { |         'pm1': { | ||||||
|             'device_class': SensorDeviceClass.PM1, |             'device_class': SensorDeviceClass.PM1, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|  |             'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER | ||||||
|         }, |         }, | ||||||
|         'atmospheric-pressure': { |         'atmospheric-pressure': { | ||||||
|             'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE, |             'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|  |             'unit_of_measurement': UnitOfPressure.PA | ||||||
|         }, |         }, | ||||||
|         'tvoc-density': { |         'tvoc-density': { | ||||||
|             'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, |             'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT | ||||||
|         }, |         }, | ||||||
|         'voc-density': 'tvoc-density', |         'voc-density': 'tvoc-density', | ||||||
|         'battery-level': { |         'battery-level': { | ||||||
|             'device_class': SensorDeviceClass.BATTERY, |             'device_class': SensorDeviceClass.BATTERY, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|  |             'unit_of_measurement': PERCENTAGE | ||||||
|         }, |         }, | ||||||
|         'voltage': { |         'voltage': { | ||||||
|             'device_class': SensorDeviceClass.VOLTAGE, |             'device_class': SensorDeviceClass.VOLTAGE, | ||||||
|             'entity': 'sensor', |             'entity': 'sensor', | ||||||
|             'optional': { |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|                 'state_class': SensorStateClass.MEASUREMENT, |             'unit_of_measurement': UnitOfElectricPotential.VOLT | ||||||
|                 'unit_of_measurement': UnitOfElectricPotential.VOLT |  | ||||||
|             } |  | ||||||
|         }, |         }, | ||||||
|         'electric-current': { |         'electric-current': { | ||||||
|             'device_class': SensorDeviceClass.CURRENT, |             'device_class': SensorDeviceClass.CURRENT, | ||||||
|             'entity': 'sensor', |             'entity': 'sensor', | ||||||
|             'optional': { |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|                 'state_class': SensorStateClass.MEASUREMENT, |             'unit_of_measurement': UnitOfElectricCurrent.AMPERE | ||||||
|                 'unit_of_measurement': UnitOfElectricCurrent.AMPERE |  | ||||||
|             } |  | ||||||
|         }, |         }, | ||||||
|         'illumination': { |         'illumination': { | ||||||
|             'device_class': SensorDeviceClass.ILLUMINANCE, |             'device_class': SensorDeviceClass.ILLUMINANCE, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor', | ||||||
|  |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|  |             'unit_of_measurement': LIGHT_LUX | ||||||
|         }, |         }, | ||||||
|         'no-one-determine-time': { |         'no-one-determine-time': { | ||||||
|             'device_class': SensorDeviceClass.DURATION, |             'device_class': SensorDeviceClass.DURATION, | ||||||
|             'entity': 'sensor' |             'entity': 'sensor' | ||||||
|         }, |         }, | ||||||
|  |         'has-someone-duration': 'no-one-determine-time', | ||||||
|  |         'no-one-duration': 'no-one-determine-time', | ||||||
|         'electric-power': { |         'electric-power': { | ||||||
|             'device_class': SensorDeviceClass.POWER, |             'device_class': SensorDeviceClass.POWER, | ||||||
|             'entity': 'sensor', |             'entity': 'sensor', | ||||||
|             'optional': { |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|                 'state_class': SensorStateClass.MEASUREMENT, |             'unit_of_measurement': UnitOfPower.WATT | ||||||
|                 'unit_of_measurement': UnitOfPower.WATT |  | ||||||
|             } |  | ||||||
|         }, |         }, | ||||||
|         'surge-power': { |         'surge-power': { | ||||||
|             'device_class': SensorDeviceClass.POWER, |             'device_class': SensorDeviceClass.POWER, | ||||||
|             'entity': 'sensor', |             'entity': 'sensor', | ||||||
|             'optional': { |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|                 'state_class': SensorStateClass.MEASUREMENT, |             'unit_of_measurement': UnitOfPower.WATT | ||||||
|                 'unit_of_measurement': UnitOfPower.WATT |  | ||||||
|             } |  | ||||||
|         }, |         }, | ||||||
|         'power-consumption': { |         'power-consumption': { | ||||||
|             'device_class': SensorDeviceClass.ENERGY, |             'device_class': SensorDeviceClass.ENERGY, | ||||||
|             'entity': 'sensor', |             'entity': 'sensor', | ||||||
|             'optional': { |             'state_class': SensorStateClass.TOTAL_INCREASING, | ||||||
|                 'state_class': SensorStateClass.TOTAL_INCREASING, |             'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR | ||||||
|                 'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR |  | ||||||
|             } |  | ||||||
|         }, |         }, | ||||||
|         'power': { |         'power': { | ||||||
|             'device_class': SensorDeviceClass.POWER, |             'device_class': SensorDeviceClass.POWER, | ||||||
|             'entity': 'sensor', |             'entity': 'sensor', | ||||||
|             'optional': { |             'state_class': SensorStateClass.MEASUREMENT, | ||||||
|                 'state_class': SensorStateClass.MEASUREMENT, |             'unit_of_measurement': UnitOfPower.WATT | ||||||
|                 'unit_of_measurement': UnitOfPower.WATT |  | ||||||
|             } |  | ||||||
|         }, |         }, | ||||||
|         'total-battery': { |         'total-battery': { | ||||||
|             'device_class': SensorDeviceClass.ENERGY, |             'device_class': SensorDeviceClass.ENERGY, | ||||||
|             'entity': 'sensor', |             'entity': 'sensor', | ||||||
|             'optional': { |             'state_class': SensorStateClass.TOTAL_INCREASING, | ||||||
|                 'state_class': SensorStateClass.TOTAL_INCREASING, |             'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR | ||||||
|                 'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR |         } | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         'has-someone-duration': 'no-one-determine-time', |  | ||||||
|         'no-one-duration': 'no-one-determine-time' |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -95,7 +95,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity): | |||||||
|         # Set device_class |         # Set device_class | ||||||
|         if self._value_list: |         if self._value_list: | ||||||
|             self._attr_device_class = SensorDeviceClass.ENUM |             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_native_unit_of_measurement = None | ||||||
|             self._attr_options = self._value_list.descriptions |             self._attr_options = self._value_list.descriptions | ||||||
|         else: |         else: | ||||||
| @@ -109,6 +109,9 @@ class Sensor(MIoTPropertyEntity, SensorEntity): | |||||||
|                     self._attr_device_class, None)  # type: ignore |                     self._attr_device_class, None)  # type: ignore | ||||||
|                 self._attr_native_unit_of_measurement = list( |                 self._attr_native_unit_of_measurement = list( | ||||||
|                     unit_sets)[0] if unit_sets else None |                     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 |             # Set state_class | ||||||
|             if spec.state_class: |             if spec.state_class: | ||||||
|                 self._attr_state_class = 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( | SPEC_FILTER_FILE = path.join( | ||||||
|     ROOT_PATH, |     ROOT_PATH, | ||||||
|     '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml') |     '../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]: | 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: | def save_yaml_file(file_path: str, data: dict) -> None: | ||||||
|     with open(file_path, 'w', encoding='utf-8') as file: |     with open(file_path, 'w', encoding='utf-8') as file: | ||||||
|         yaml.safe_dump( |         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: | def dict_str_str(d: dict) -> bool: | ||||||
| @@ -135,6 +139,21 @@ def bool_trans(d: dict) -> bool: | |||||||
|     return True |     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: | def compare_dict_structure(dict1: dict, dict2: dict) -> bool: | ||||||
|     if not isinstance(dict1, dict) or not isinstance(dict2, dict): |     if not isinstance(dict1, dict) or not isinstance(dict2, dict): | ||||||
|         _LOGGER.info('invalid type') |         _LOGGER.info('invalid type') | ||||||
| @@ -181,6 +200,12 @@ def sort_spec_filter(file_path: str): | |||||||
|     return filter_data |     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 | @pytest.mark.github | ||||||
| def test_bool_trans(): | def test_bool_trans(): | ||||||
|     data = load_yaml_file(SPEC_BOOL_TRANS_FILE) |     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' |     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 | @pytest.mark.github | ||||||
| def test_miot_i18n(): | def test_miot_i18n(): | ||||||
|     for file_name in listdir(MIOT_I18N_RELATIVE_PATH): |     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) |     sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE) | ||||||
|     save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data) |     save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data) | ||||||
|     _LOGGER.info('%s formatted.', SPEC_FILTER_FILE) |     _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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user