mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2026-01-16 06:30:44 +08:00
Compare commits
1 Commits
ee91c30ecc
...
fbebfc4fd8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbebfc4fd8 |
@ -98,9 +98,6 @@ footer: Optional. The footer is the place to reference GitHub issues and PRs tha
|
||||
|
||||
When contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](../LICENSE.md).
|
||||
|
||||
|
||||
When you submit your first pull request, GitHub Action will prompt you to sign the Contributor License Agreement (CLA). Only after you sign the CLA, your pull request will be merged.
|
||||
|
||||
## How to Get Help
|
||||
|
||||
If you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub.
|
||||
|
||||
@ -62,7 +62,7 @@ from homeassistant.components.climate import (
|
||||
ATTR_TEMPERATURE,
|
||||
HVACMode,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature
|
||||
ClimateEntityFeature,
|
||||
)
|
||||
|
||||
from .miot.const import DOMAIN
|
||||
@ -75,7 +75,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a config entry."""
|
||||
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
|
||||
@ -211,14 +211,14 @@ class FeaturePresetMode(MIoTServiceEntity, ClimateEntity):
|
||||
"""Set the preset mode."""
|
||||
await self.set_property_async(
|
||||
self._prop_mode,
|
||||
value=self.get_map_key(map_=self._mode_map, value=preset_mode))
|
||||
value=self.get_map_value(map_=self._mode_map, key=preset_mode))
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> Optional[str]:
|
||||
return (
|
||||
self.get_map_value(
|
||||
self.get_map_key(
|
||||
map_=self._mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
value=self.get_prop_value(prop=self._prop_mode))
|
||||
if self._prop_mode else None)
|
||||
|
||||
|
||||
@ -265,8 +265,8 @@ class FeatureFanMode(MIoTServiceEntity, ClimateEntity):
|
||||
if fan_mode == FAN_ON:
|
||||
await self.set_property_async(prop=self._prop_fan_on, value=True)
|
||||
return
|
||||
mode_value = self.get_map_key(
|
||||
map_=self._fan_mode_map, value=fan_mode)
|
||||
mode_value = self.get_map_value(
|
||||
map_=self._fan_mode_map, key=fan_mode)
|
||||
if mode_value is None or not await self.set_property_async(
|
||||
prop=self._prop_fan_level, value=mode_value
|
||||
):
|
||||
@ -283,9 +283,9 @@ class FeatureFanMode(MIoTServiceEntity, ClimateEntity):
|
||||
return (
|
||||
FAN_ON if self.get_prop_value(prop=self._prop_fan_on)
|
||||
else FAN_OFF)
|
||||
return self.get_map_value(
|
||||
return self.get_map_key(
|
||||
map_=self._fan_mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_fan_level))
|
||||
value=self.get_prop_value(prop=self._prop_fan_level))
|
||||
|
||||
|
||||
class FeatureSwingMode(MIoTServiceEntity, ClimateEntity):
|
||||
@ -457,7 +457,7 @@ class Heater(
|
||||
FeatureTargetTemperature,
|
||||
FeatureTemperature,
|
||||
FeatureHumidity,
|
||||
FeaturePresetMode
|
||||
FeaturePresetMode,
|
||||
):
|
||||
"""Heater"""
|
||||
|
||||
@ -492,7 +492,7 @@ class AirConditioner(
|
||||
FeatureTemperature,
|
||||
FeatureHumidity,
|
||||
FeatureFanMode,
|
||||
FeatureSwingMode
|
||||
FeatureSwingMode,
|
||||
):
|
||||
"""Air conditioner"""
|
||||
_prop_mode: Optional[MIoTSpecProperty]
|
||||
@ -562,8 +562,8 @@ class AirConditioner(
|
||||
# set mode
|
||||
if self._prop_mode is None:
|
||||
return
|
||||
mode_value = self.get_map_key(
|
||||
map_=self._hvac_mode_map, value=hvac_mode)
|
||||
mode_value = self.get_map_value(
|
||||
map_=self._hvac_mode_map, key=hvac_mode)
|
||||
if mode_value is None or not await self.set_property_async(
|
||||
prop=self._prop_mode, value=mode_value
|
||||
):
|
||||
@ -576,9 +576,9 @@ class AirConditioner(
|
||||
if self.get_prop_value(prop=self._prop_on) is False:
|
||||
return HVACMode.OFF
|
||||
return (
|
||||
self.get_map_value(
|
||||
self.get_map_key(
|
||||
map_=self._hvac_mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
value=self.get_prop_value(prop=self._prop_mode))
|
||||
if self._prop_mode else None)
|
||||
|
||||
def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None:
|
||||
@ -611,8 +611,8 @@ class AirConditioner(
|
||||
if mode:
|
||||
self.set_prop_value(
|
||||
prop=self._prop_mode,
|
||||
value=self.get_map_key(
|
||||
map_=self._hvac_mode_map, value=mode))
|
||||
value=self.get_map_value(
|
||||
map_=self._hvac_mode_map, key=mode))
|
||||
# T: target temperature
|
||||
if 'T' in v_ac_state and self._prop_target_temp:
|
||||
self.set_prop_value(
|
||||
@ -645,7 +645,7 @@ class PtcBathHeater(
|
||||
FeatureTargetTemperature,
|
||||
FeatureTemperature,
|
||||
FeatureFanMode,
|
||||
FeatureSwingMode
|
||||
FeatureSwingMode,
|
||||
):
|
||||
"""Ptc bath heater"""
|
||||
_prop_mode: Optional[MIoTSpecProperty]
|
||||
@ -688,8 +688,8 @@ class PtcBathHeater(
|
||||
"""Set the target hvac mode."""
|
||||
if self._prop_mode is None:
|
||||
return
|
||||
mode_value = self.get_map_key(
|
||||
map_=self._hvac_mode_map, value=hvac_mode)
|
||||
mode_value = self.get_map_value(
|
||||
map_=self._hvac_mode_map, key=hvac_mode)
|
||||
if mode_value is None or not await self.set_property_async(
|
||||
prop=self._prop_mode, value=mode_value
|
||||
):
|
||||
@ -711,7 +711,7 @@ class Thermostat(
|
||||
FeatureTargetTemperature,
|
||||
FeatureTemperature,
|
||||
FeatureHumidity,
|
||||
FeatureFanMode
|
||||
FeatureFanMode,
|
||||
):
|
||||
"""Thermostat"""
|
||||
_prop_mode: Optional[MIoTSpecProperty]
|
||||
@ -772,8 +772,8 @@ class Thermostat(
|
||||
# set mode
|
||||
if self._prop_mode is None:
|
||||
return
|
||||
mode_value = self.get_map_key(
|
||||
map_=self._hvac_mode_map, value=hvac_mode
|
||||
mode_value = self.get_map_value(
|
||||
map_=self._hvac_mode_map, key=hvac_mode
|
||||
)
|
||||
if mode_value is None or not await self.set_property_async(
|
||||
prop=self._prop_mode, value=mode_value
|
||||
@ -787,7 +787,7 @@ class Thermostat(
|
||||
if self.get_prop_value(prop=self._prop_on) is False:
|
||||
return HVACMode.OFF
|
||||
return (
|
||||
self.get_map_value(
|
||||
self.get_map_key(
|
||||
map_=self._hvac_mode_map,
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
value=self.get_prop_value(prop=self._prop_mode))
|
||||
if self._prop_mode else None)
|
||||
|
||||
@ -150,7 +150,7 @@ class MIoTClient:
|
||||
# Device list update timestamp
|
||||
_device_list_update_ts: int
|
||||
|
||||
_sub_source_list: dict[str, Optional[str]]
|
||||
_sub_source_list: dict[str, str]
|
||||
_sub_tree: MIoTMatcher
|
||||
_sub_device_state: dict[str, MipsDeviceState]
|
||||
|
||||
@ -620,7 +620,7 @@ class MIoTClient:
|
||||
# Priority local control
|
||||
if self._ctrl_mode == CtrlMode.AUTO:
|
||||
# Gateway control
|
||||
device_gw = self._device_list_gateway.get(did, None)
|
||||
device_gw: dict = self._device_list_gateway.get(did, None)
|
||||
if (
|
||||
device_gw and device_gw.get('online', False)
|
||||
and device_gw.get('specv2_access', False)
|
||||
@ -641,7 +641,7 @@ class MIoTClient:
|
||||
raise MIoTClientError(
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# Lan control
|
||||
device_lan = self._device_list_lan.get(did, None)
|
||||
device_lan: dict = self._device_list_lan.get(did, None)
|
||||
if device_lan and device_lan.get('online', False):
|
||||
result = await self._miot_lan.set_prop_async(
|
||||
did=did, siid=siid, piid=piid, value=value)
|
||||
@ -657,7 +657,7 @@ class MIoTClient:
|
||||
# Cloud control
|
||||
device_cloud = self._device_list_cloud.get(did, None)
|
||||
if device_cloud and device_cloud.get('online', False):
|
||||
result = await self._http.set_prop_async(
|
||||
result: list = await self._http.set_prop_async(
|
||||
params=[
|
||||
{'did': did, 'siid': siid, 'piid': piid, 'value': value}
|
||||
])
|
||||
@ -746,7 +746,7 @@ class MIoTClient:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
|
||||
device_gw = self._device_list_gateway.get(did, None)
|
||||
device_gw: dict = self._device_list_gateway.get(did, None)
|
||||
# Priority local control
|
||||
if self._ctrl_mode == CtrlMode.AUTO:
|
||||
if (
|
||||
@ -782,7 +782,7 @@ class MIoTClient:
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# Cloud control
|
||||
device_cloud = self._device_list_cloud.get(did, None)
|
||||
if device_cloud and device_cloud.get('online', False):
|
||||
if device_cloud.get('online', False):
|
||||
result: dict = await self._http.action_async(
|
||||
did=did, siid=siid, aiid=aiid, in_list=in_list)
|
||||
if result:
|
||||
@ -798,15 +798,14 @@ class MIoTClient:
|
||||
dids=[did]))
|
||||
raise MIoTClientError(
|
||||
self.__get_exec_error_with_rc(rc=rc))
|
||||
# TODO: Show error message
|
||||
# Show error message
|
||||
_LOGGER.error(
|
||||
'client action failed, %s.%d.%d', did, siid, aiid)
|
||||
return []
|
||||
return None
|
||||
|
||||
def sub_prop(
|
||||
self, did: str, handler: Callable[[dict, Any], None],
|
||||
siid: Optional[int] = None, piid: Optional[int] = None,
|
||||
handler_ctx: Any = None
|
||||
siid: int = None, piid: int = None, handler_ctx: Any = None
|
||||
) -> bool:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
@ -819,9 +818,7 @@ class MIoTClient:
|
||||
_LOGGER.debug('client sub prop, %s', topic)
|
||||
return True
|
||||
|
||||
def unsub_prop(
|
||||
self, did: str, siid: Optional[int] = None, piid: Optional[int] = None
|
||||
) -> bool:
|
||||
def unsub_prop(self, did: str, siid: int = None, piid: int = None) -> bool:
|
||||
topic = (
|
||||
f'{did}/p/'
|
||||
f'{"#" if siid is None or piid is None else f"{siid}/{piid}"}')
|
||||
@ -832,8 +829,7 @@ class MIoTClient:
|
||||
|
||||
def sub_event(
|
||||
self, did: str, handler: Callable[[dict, Any], None],
|
||||
siid: Optional[int] = None, eiid: Optional[int] = None,
|
||||
handler_ctx: Any = None
|
||||
siid: int = None, eiid: int = None, handler_ctx: Any = None
|
||||
) -> bool:
|
||||
if did not in self._device_list_cache:
|
||||
raise MIoTClientError(f'did not exist, {did}')
|
||||
@ -845,9 +841,7 @@ class MIoTClient:
|
||||
_LOGGER.debug('client sub event, %s', topic)
|
||||
return True
|
||||
|
||||
def unsub_event(
|
||||
self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None
|
||||
) -> bool:
|
||||
def unsub_event(self, did: str, siid: int = None, eiid: int = None) -> bool:
|
||||
topic = (
|
||||
f'{did}/e/'
|
||||
f'{"#" if siid is None or eiid is None else f"{siid}/{eiid}"}')
|
||||
@ -1087,7 +1081,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self.__request_show_devices_changed_notify()
|
||||
@ -1097,8 +1091,8 @@ class MIoTClient:
|
||||
self, group_id: str, state: bool
|
||||
) -> None:
|
||||
_LOGGER.info('local mips state changed, %s, %s', group_id, state)
|
||||
mips = self._mips_local.get(group_id, None)
|
||||
if not mips:
|
||||
mips: MipsLocalClient = self._mips_local.get(group_id, None)
|
||||
if mips is None:
|
||||
_LOGGER.error(
|
||||
'local mips state changed, mips not exist, %s', group_id)
|
||||
return
|
||||
@ -1130,7 +1124,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self.__request_show_devices_changed_notify()
|
||||
@ -1177,7 +1171,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)
|
||||
self._device_list_lan = {}
|
||||
@ -1207,7 +1201,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
return
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1263,7 +1257,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
return
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1307,8 +1301,9 @@ class MIoTClient:
|
||||
async def __load_cache_device_async(self) -> None:
|
||||
"""Load device list from cache."""
|
||||
cache_list: Optional[dict[str, dict]] = await self._storage.load_async(
|
||||
domain='miot_devices', name=f'{self._uid}_{self._cloud_server}',
|
||||
type_=dict) # type: ignore
|
||||
domain='miot_devices',
|
||||
name=f'{self._uid}_{self._cloud_server}',
|
||||
type_=dict)
|
||||
if not cache_list:
|
||||
self.__show_client_error_notify(
|
||||
message=self._i18n.translate(
|
||||
@ -1351,7 +1346,7 @@ class MIoTClient:
|
||||
cloud_state_old: Optional[bool] = self._device_list_cloud.get(
|
||||
did, {}).get('online', None)
|
||||
cloud_state_new: Optional[bool] = None
|
||||
device_new = cloud_list.pop(did, None)
|
||||
device_new: dict = cloud_list.pop(did, None)
|
||||
if device_new:
|
||||
cloud_state_new = device_new.get('online', None)
|
||||
# Update cache device info
|
||||
@ -1376,7 +1371,7 @@ class MIoTClient:
|
||||
continue
|
||||
info['online'] = state_new
|
||||
# Call device state changed callback
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1431,7 +1426,8 @@ class MIoTClient:
|
||||
self, dids: list[str]
|
||||
) -> None:
|
||||
_LOGGER.debug('refresh cloud device with dids, %s', dids)
|
||||
cloud_list = await self._http.get_devices_with_dids_async(dids=dids)
|
||||
cloud_list: dict[str, dict] = (
|
||||
await self._http.get_devices_with_dids_async(dids=dids))
|
||||
if cloud_list is None:
|
||||
_LOGGER.error('cloud http get_dev_list_async failed, %s', dids)
|
||||
return
|
||||
@ -1470,11 +1466,11 @@ class MIoTClient:
|
||||
for did, info in self._device_list_cache.items():
|
||||
if did not in filter_dids:
|
||||
continue
|
||||
device_old = self._device_list_gateway.get(did, None)
|
||||
device_old: dict = self._device_list_gateway.get(did, None)
|
||||
gw_state_old = device_old.get(
|
||||
'online', False) if device_old else False
|
||||
gw_state_new: bool = False
|
||||
device_new = gw_list.pop(did, None)
|
||||
device_new: dict = gw_list.pop(did, None)
|
||||
if device_new:
|
||||
# Update gateway device info
|
||||
self._device_list_gateway[did] = {
|
||||
@ -1497,7 +1493,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
info['online'] = state_new
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1522,7 +1518,7 @@ class MIoTClient:
|
||||
if state_old == state_new:
|
||||
continue
|
||||
self._device_list_cache[did]['online'] = state_new
|
||||
sub = self._sub_device_state.get(did, None)
|
||||
sub: MipsDeviceState = self._sub_device_state.get(did, None)
|
||||
if sub and sub.handler:
|
||||
sub.handler(
|
||||
did, MIoTDeviceState.ONLINE if state_new
|
||||
@ -1537,7 +1533,7 @@ class MIoTClient:
|
||||
'refresh gw devices with group_id, %s', group_id)
|
||||
# Remove timer
|
||||
self._mips_local_state_changed_timers.pop(group_id, None)
|
||||
mips = self._mips_local.get(group_id, None)
|
||||
mips: MipsLocalClient = self._mips_local.get(group_id, None)
|
||||
if not mips:
|
||||
_LOGGER.error('mips not exist, %s', group_id)
|
||||
return
|
||||
@ -1904,73 +1900,77 @@ async def get_miot_instance_async(
|
||||
) -> MIoTClient:
|
||||
if entry_id is None:
|
||||
raise MIoTClientError('invalid entry_id')
|
||||
miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None)
|
||||
if miot_client:
|
||||
miot_client: MIoTClient = None
|
||||
if a := hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None):
|
||||
_LOGGER.info('instance exist, %s', entry_id)
|
||||
return miot_client
|
||||
# Create new instance
|
||||
if not entry_data:
|
||||
raise MIoTClientError('entry data is None')
|
||||
# Get running loop
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||
if not loop:
|
||||
raise MIoTClientError('loop is None')
|
||||
# MIoT storage
|
||||
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
|
||||
'miot_storage', None)
|
||||
if not storage:
|
||||
storage = MIoTStorage(
|
||||
root_path=entry_data['storage_path'], loop=loop)
|
||||
hass.data[DOMAIN]['miot_storage'] = storage
|
||||
_LOGGER.info('create miot_storage instance')
|
||||
global_config: dict = await storage.load_user_config_async(
|
||||
uid='global_config', cloud_server='all',
|
||||
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
|
||||
# MIoT network
|
||||
network_detect_addr: dict = global_config.get('network_detect_addr', {})
|
||||
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
|
||||
'miot_network', None)
|
||||
if not network:
|
||||
network = MIoTNetwork(
|
||||
ip_addr_list=network_detect_addr.get('ip', []),
|
||||
url_addr_list=network_detect_addr.get('url', []),
|
||||
refresh_interval=NETWORK_REFRESH_INTERVAL,
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_network'] = network
|
||||
await network.init_async()
|
||||
_LOGGER.info('create miot_network instance')
|
||||
# MIoT service
|
||||
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
|
||||
'mips_service', None)
|
||||
if not mips_service:
|
||||
aiozc = await zeroconf.async_get_async_instance(hass)
|
||||
mips_service = MipsService(aiozc=aiozc, loop=loop)
|
||||
hass.data[DOMAIN]['mips_service'] = mips_service
|
||||
await mips_service.init_async()
|
||||
_LOGGER.info('create mips_service instance')
|
||||
# MIoT lan
|
||||
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None)
|
||||
if not miot_lan:
|
||||
miot_lan = MIoTLan(
|
||||
net_ifs=global_config.get('net_interfaces', []),
|
||||
miot_client = a
|
||||
else:
|
||||
if entry_data is None:
|
||||
raise MIoTClientError('entry data is None')
|
||||
# Get running loop
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
|
||||
if loop is None:
|
||||
raise MIoTClientError('loop is None')
|
||||
# MIoT storage
|
||||
storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(
|
||||
'miot_storage', None)
|
||||
if not storage:
|
||||
storage = MIoTStorage(
|
||||
root_path=entry_data['storage_path'], loop=loop)
|
||||
hass.data[DOMAIN]['miot_storage'] = storage
|
||||
_LOGGER.info('create miot_storage instance')
|
||||
global_config: dict = await storage.load_user_config_async(
|
||||
uid='global_config', cloud_server='all',
|
||||
keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])
|
||||
# MIoT network
|
||||
network_detect_addr: dict = global_config.get(
|
||||
'network_detect_addr', {})
|
||||
network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(
|
||||
'miot_network', None)
|
||||
if not network:
|
||||
network = MIoTNetwork(
|
||||
ip_addr_list=network_detect_addr.get('ip', []),
|
||||
url_addr_list=network_detect_addr.get('url', []),
|
||||
refresh_interval=NETWORK_REFRESH_INTERVAL,
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_network'] = network
|
||||
await network.init_async()
|
||||
_LOGGER.info('create miot_network instance')
|
||||
# MIoT service
|
||||
mips_service: Optional[MipsService] = hass.data[DOMAIN].get(
|
||||
'mips_service', None)
|
||||
if not mips_service:
|
||||
aiozc = await zeroconf.async_get_async_instance(hass)
|
||||
mips_service = MipsService(aiozc=aiozc, loop=loop)
|
||||
hass.data[DOMAIN]['mips_service'] = mips_service
|
||||
await mips_service.init_async()
|
||||
_LOGGER.info('create mips_service instance')
|
||||
# MIoT lan
|
||||
miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get(
|
||||
'miot_lan', None)
|
||||
if not miot_lan:
|
||||
miot_lan = MIoTLan(
|
||||
net_ifs=global_config.get('net_interfaces', []),
|
||||
network=network,
|
||||
mips_service=mips_service,
|
||||
enable_subscribe=global_config.get('enable_subscribe', False),
|
||||
loop=loop)
|
||||
hass.data[DOMAIN]['miot_lan'] = miot_lan
|
||||
_LOGGER.info('create miot_lan instance')
|
||||
# MIoT client
|
||||
miot_client = MIoTClient(
|
||||
entry_id=entry_id,
|
||||
entry_data=entry_data,
|
||||
network=network,
|
||||
storage=storage,
|
||||
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,
|
||||
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()
|
||||
miot_lan=miot_lan,
|
||||
loop=loop
|
||||
)
|
||||
miot_client.persistent_notify = persistent_notify
|
||||
hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)
|
||||
_LOGGER.info(
|
||||
'new miot_client instance, %s, %s', entry_id, entry_data)
|
||||
await miot_client.init_async()
|
||||
|
||||
return miot_client
|
||||
|
||||
@ -382,7 +382,7 @@ class MIoTHttpClient:
|
||||
|
||||
return res_obj['data']
|
||||
|
||||
async def get_central_cert_async(self, csr: str) -> str:
|
||||
async def get_central_cert_async(self, csr: str) -> Optional[str]:
|
||||
if not isinstance(csr, str):
|
||||
raise MIoTHttpError('invalid params')
|
||||
|
||||
|
||||
@ -56,7 +56,6 @@ from homeassistant.const import (
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
DEGREE,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
@ -73,7 +72,6 @@ from homeassistant.const import (
|
||||
UnitOfPower,
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
UnitOfDataRate
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
@ -245,12 +243,12 @@ class MIoTDevice:
|
||||
def sub_device_state(
|
||||
self, key: str, handler: Callable[[str, MIoTDeviceState], None]
|
||||
) -> int:
|
||||
sub_id = self.__gen_sub_id()
|
||||
self._sub_id += 1
|
||||
if key in self._device_state_sub_list:
|
||||
self._device_state_sub_list[key][str(sub_id)] = handler
|
||||
self._device_state_sub_list[key][str(self._sub_id)] = handler
|
||||
else:
|
||||
self._device_state_sub_list[key] = {str(sub_id): handler}
|
||||
return sub_id
|
||||
self._device_state_sub_list[key] = {str(self._sub_id): handler}
|
||||
return self._sub_id
|
||||
|
||||
def unsub_device_state(self, key: str, sub_id: int) -> None:
|
||||
sub_list = self._device_state_sub_list.get(key, None)
|
||||
@ -268,14 +266,14 @@ class MIoTDevice:
|
||||
for handler in self._value_sub_list[key].values():
|
||||
handler(params, ctx)
|
||||
|
||||
sub_id = self.__gen_sub_id()
|
||||
self._sub_id += 1
|
||||
if key in self._value_sub_list:
|
||||
self._value_sub_list[key][str(sub_id)] = handler
|
||||
self._value_sub_list[key][str(self._sub_id)] = handler
|
||||
else:
|
||||
self._value_sub_list[key] = {str(sub_id): handler}
|
||||
self._value_sub_list[key] = {str(self._sub_id): handler}
|
||||
self.miot_client.sub_prop(
|
||||
did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)
|
||||
return sub_id
|
||||
return self._sub_id
|
||||
|
||||
def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:
|
||||
key: str = f'p.{siid}.{piid}'
|
||||
@ -296,14 +294,14 @@ class MIoTDevice:
|
||||
for handler in self._value_sub_list[key].values():
|
||||
handler(params, ctx)
|
||||
|
||||
sub_id = self.__gen_sub_id()
|
||||
self._sub_id += 1
|
||||
if key in self._value_sub_list:
|
||||
self._value_sub_list[key][str(sub_id)] = handler
|
||||
self._value_sub_list[key][str(self._sub_id)] = handler
|
||||
else:
|
||||
self._value_sub_list[key] = {str(sub_id): handler}
|
||||
self._value_sub_list[key] = {str(self._sub_id): handler}
|
||||
self.miot_client.sub_event(
|
||||
did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)
|
||||
return sub_id
|
||||
return self._sub_id
|
||||
|
||||
def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:
|
||||
key: str = f'e.{siid}.{eiid}'
|
||||
@ -416,12 +414,10 @@ class MIoTDevice:
|
||||
spec_name: str = spec_instance.name
|
||||
if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str):
|
||||
spec_name = SPEC_DEVICE_TRANS_MAP[spec_name]
|
||||
if 'required' not in SPEC_DEVICE_TRANS_MAP[spec_name]:
|
||||
return None
|
||||
# 1. The device shall have all required services.
|
||||
required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys()
|
||||
if not {
|
||||
service.name for service in spec_instance.services
|
||||
service.name for service in spec_instance.services
|
||||
}.issuperset(required_services):
|
||||
return None
|
||||
optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys()
|
||||
@ -431,13 +427,9 @@ class MIoTDevice:
|
||||
for service in spec_instance.services:
|
||||
if service.platform:
|
||||
continue
|
||||
required_properties: dict
|
||||
optional_properties: dict
|
||||
required_actions: set
|
||||
optional_actions: set
|
||||
# 2. The service shall have all required properties, actions.
|
||||
if service.name in required_services:
|
||||
required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
'required'].get(
|
||||
service.name, {}
|
||||
).get('required', {}).get('properties', {})
|
||||
@ -454,7 +446,7 @@ class MIoTDevice:
|
||||
service.name, {}
|
||||
).get('optional', {}).get('actions', set({}))
|
||||
elif service.name in optional_services:
|
||||
required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
required_properties: dict = SPEC_DEVICE_TRANS_MAP[spec_name][
|
||||
'optional'].get(
|
||||
service.name, {}
|
||||
).get('required', {}).get('properties', {})
|
||||
@ -492,7 +484,7 @@ class MIoTDevice:
|
||||
set(required_properties.keys()), optional_properties):
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
# prop.icon = self.icon_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
prop.platform = platform
|
||||
entity_data.props.add(prop)
|
||||
# action
|
||||
@ -507,95 +499,85 @@ class MIoTDevice:
|
||||
return entity_data
|
||||
|
||||
def parse_miot_service_entity(
|
||||
self, miot_service: MIoTSpecService
|
||||
self, service_instance: MIoTSpecService
|
||||
) -> Optional[MIoTEntityData]:
|
||||
if (
|
||||
miot_service.platform
|
||||
or miot_service.name not in SPEC_SERVICE_TRANS_MAP
|
||||
):
|
||||
service = service_instance
|
||||
if service.platform or (service.name not in SPEC_SERVICE_TRANS_MAP):
|
||||
return None
|
||||
service_name = miot_service.name
|
||||
|
||||
service_name = service.name
|
||||
if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str):
|
||||
service_name = SPEC_SERVICE_TRANS_MAP[service_name]
|
||||
if 'required' not in SPEC_SERVICE_TRANS_MAP[service_name]:
|
||||
return None
|
||||
# Required properties, required access mode
|
||||
# 1. The service shall have all required properties.
|
||||
required_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][
|
||||
'required'].get('properties', {})
|
||||
if not {
|
||||
prop.name for prop in miot_service.properties if prop.access
|
||||
prop.name for prop in service.properties if prop.access
|
||||
}.issuperset(set(required_properties.keys())):
|
||||
return None
|
||||
for prop in miot_service.properties:
|
||||
# 2. The required property shall have all required access mode.
|
||||
for prop in service.properties:
|
||||
if prop.name in required_properties:
|
||||
if not set(prop.access).issuperset(
|
||||
required_properties[prop.name]):
|
||||
return None
|
||||
# Required actions
|
||||
# Required events
|
||||
platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity']
|
||||
entity_data = MIoTEntityData(platform=platform, spec=miot_service)
|
||||
# Optional properties
|
||||
entity_data = MIoTEntityData(platform=platform, spec=service_instance)
|
||||
optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][
|
||||
'optional'].get('properties', set({}))
|
||||
for prop in miot_service.properties:
|
||||
for prop in service.properties:
|
||||
if prop.name in set.union(
|
||||
set(required_properties.keys()), optional_properties):
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
# prop.icon = self.icon_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
prop.platform = platform
|
||||
entity_data.props.add(prop)
|
||||
# Optional actions
|
||||
# Optional events
|
||||
miot_service.platform = platform
|
||||
# action
|
||||
# event
|
||||
# No actions or events is in SPEC_SERVICE_TRANS_MAP now.
|
||||
service.platform = platform
|
||||
return entity_data
|
||||
|
||||
def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:
|
||||
def parse_miot_property_entity(
|
||||
self, property_instance: MIoTSpecProperty
|
||||
) -> Optional[dict[str, str]]:
|
||||
prop = property_instance
|
||||
if (
|
||||
miot_prop.platform
|
||||
or miot_prop.name not in SPEC_PROP_TRANS_MAP['properties']
|
||||
prop.platform
|
||||
or (prop.name not in SPEC_PROP_TRANS_MAP['properties'])
|
||||
):
|
||||
return False
|
||||
prop_name = miot_prop.name
|
||||
return None
|
||||
|
||||
prop_name = prop.name
|
||||
if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str):
|
||||
prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name]
|
||||
platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity']
|
||||
# Check
|
||||
prop_access: set = set({})
|
||||
if miot_prop.readable:
|
||||
if prop.readable:
|
||||
prop_access.add('read')
|
||||
if miot_prop.writable:
|
||||
if prop.writable:
|
||||
prop_access.add('write')
|
||||
if prop_access != (SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['access']):
|
||||
return False
|
||||
if miot_prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||
return None
|
||||
if prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[
|
||||
'entities'][platform]['format']:
|
||||
return False
|
||||
miot_prop.device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||
return None
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||
'device_class']
|
||||
# Optional params
|
||||
if 'state_class' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
|
||||
miot_prop.state_class = SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]['state_class']
|
||||
if (
|
||||
not miot_prop.external_unit
|
||||
and 'unit_of_measurement' in SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]
|
||||
):
|
||||
# Priority: spec_modify.unit > unit_convert > specv2entity.unit
|
||||
miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][
|
||||
prop_name]['unit_of_measurement']
|
||||
if (
|
||||
not miot_prop.icon
|
||||
and 'icon' in SPEC_PROP_TRANS_MAP['properties'][prop_name]
|
||||
):
|
||||
# Priority: spec_modify.icon > icon_convert > specv2entity.icon
|
||||
miot_prop.icon = SPEC_PROP_TRANS_MAP['properties'][prop_name][
|
||||
'icon']
|
||||
miot_prop.platform = platform
|
||||
return True
|
||||
result = {'platform': platform, 'device_class': device_class}
|
||||
# optional:
|
||||
if 'optional' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:
|
||||
optional = SPEC_PROP_TRANS_MAP['properties'][prop_name]['optional']
|
||||
if 'state_class' in optional:
|
||||
result['state_class'] = optional['state_class']
|
||||
if not prop.unit and 'unit_of_measurement' in optional:
|
||||
result['unit_of_measurement'] = optional['unit_of_measurement']
|
||||
return result
|
||||
|
||||
def spec_transform(self) -> None:
|
||||
"""Parse service, property, event, action from device spec."""
|
||||
@ -607,7 +589,7 @@ class MIoTDevice:
|
||||
# STEP 2: service conversion
|
||||
for service in self.spec_instance.services:
|
||||
service_entity = self.parse_miot_service_entity(
|
||||
miot_service=service)
|
||||
service_instance=service)
|
||||
if service_entity:
|
||||
self.append_entity(entity_data=service_entity)
|
||||
# STEP 3.1: property conversion
|
||||
@ -616,11 +598,20 @@ class MIoTDevice:
|
||||
continue
|
||||
if prop.unit:
|
||||
prop.external_unit = self.unit_convert(prop.unit)
|
||||
if not prop.icon:
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
# Special conversion
|
||||
self.parse_miot_property_entity(miot_prop=prop)
|
||||
# General conversion
|
||||
prop.icon = self.icon_convert(prop.unit)
|
||||
prop_entity = self.parse_miot_property_entity(
|
||||
property_instance=prop)
|
||||
if prop_entity:
|
||||
prop.platform = prop_entity['platform']
|
||||
prop.device_class = prop_entity['device_class']
|
||||
if 'state_class' in prop_entity:
|
||||
prop.state_class = prop_entity['state_class']
|
||||
if 'unit_of_measurement' in prop_entity:
|
||||
prop.external_unit = self.unit_convert(
|
||||
prop_entity['unit_of_measurement'])
|
||||
prop.icon = self.icon_convert(
|
||||
prop_entity['unit_of_measurement'])
|
||||
# general conversion
|
||||
if not prop.platform:
|
||||
if prop.writable:
|
||||
if prop.format_ == str:
|
||||
@ -634,7 +625,7 @@ class MIoTDevice:
|
||||
prop.platform = 'number'
|
||||
else:
|
||||
# Irregular property will not be transformed.
|
||||
continue
|
||||
pass
|
||||
elif prop.readable or prop.notifiable:
|
||||
if prop.format_ == bool:
|
||||
prop.platform = 'binary_sensor'
|
||||
@ -662,66 +653,11 @@ class MIoTDevice:
|
||||
self.append_action(action=action)
|
||||
|
||||
def unit_convert(self, spec_unit: str) -> Optional[str]:
|
||||
"""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
|
||||
}
|
||||
"""
|
||||
"""Convert MIoT unit to Home Assistant unit."""
|
||||
unit_map = {
|
||||
'percentage': PERCENTAGE,
|
||||
'weeks': UnitOfTime.WEEKS,
|
||||
'days': UnitOfTime.DAYS,
|
||||
'hour': UnitOfTime.HOURS,
|
||||
'hours': UnitOfTime.HOURS,
|
||||
'minutes': UnitOfTime.MINUTES,
|
||||
'seconds': UnitOfTime.SECONDS,
|
||||
@ -736,48 +672,30 @@ class MIoTDevice:
|
||||
'ppb': CONCENTRATION_PARTS_PER_BILLION,
|
||||
'lux': LIGHT_LUX,
|
||||
'pascal': UnitOfPressure.PA,
|
||||
'kilopascal': UnitOfPressure.KPA,
|
||||
'mmHg': UnitOfPressure.MMHG,
|
||||
'bar': UnitOfPressure.BAR,
|
||||
'watt': UnitOfPower.WATT,
|
||||
'L': UnitOfVolume.LITERS,
|
||||
'liter': UnitOfVolume.LITERS,
|
||||
'mL': UnitOfVolume.MILLILITERS,
|
||||
'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
'm/s': UnitOfSpeed.METERS_PER_SECOND,
|
||||
'watt': UnitOfPower.WATT,
|
||||
'w': UnitOfPower.WATT,
|
||||
'W': UnitOfPower.WATT,
|
||||
'kWh': UnitOfEnergy.KILO_WATT_HOUR,
|
||||
'A': UnitOfElectricCurrent.AMPERE,
|
||||
'mA': UnitOfElectricCurrent.MILLIAMPERE,
|
||||
'V': UnitOfElectricPotential.VOLT,
|
||||
'mv': UnitOfElectricPotential.MILLIVOLT,
|
||||
'mV': UnitOfElectricPotential.MILLIVOLT,
|
||||
'cm': UnitOfLength.CENTIMETERS,
|
||||
'm': UnitOfLength.METERS,
|
||||
'meter': UnitOfLength.METERS,
|
||||
'km': UnitOfLength.KILOMETERS,
|
||||
'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
'gram': UnitOfMass.GRAMS,
|
||||
'kilogram': UnitOfMass.KILOGRAMS,
|
||||
'dB': SIGNAL_STRENGTH_DECIBELS,
|
||||
'arcdegrees': DEGREE,
|
||||
'arcdegress': DEGREE,
|
||||
'kB': UnitOfInformation.KILOBYTES,
|
||||
'MB': UnitOfInformation.MEGABYTES,
|
||||
'GB': UnitOfInformation.GIGABYTES,
|
||||
'TB': UnitOfInformation.TERABYTES,
|
||||
'B/s': UnitOfDataRate.BYTES_PER_SECOND,
|
||||
'KB/s': UnitOfDataRate.KILOBYTES_PER_SECOND,
|
||||
'MB/s': UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
'GB/s': UnitOfDataRate.GIGABYTES_PER_SECOND
|
||||
}
|
||||
|
||||
# Handle UnitOfConductivity separately since
|
||||
# it might not be available in all HA versions
|
||||
try:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from homeassistant.const import UnitOfConductivity # type: ignore
|
||||
from homeassistant.const import UnitOfConductivity
|
||||
unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM
|
||||
except Exception: # pylint: disable=broad-except
|
||||
unit_map['μS/cm'] = 'μS/cm'
|
||||
@ -785,66 +703,59 @@ class MIoTDevice:
|
||||
return unit_map.get(spec_unit, None)
|
||||
|
||||
def icon_convert(self, spec_unit: str) -> Optional[str]:
|
||||
if spec_unit in {'percentage'}:
|
||||
if spec_unit in ['percentage']:
|
||||
return 'mdi:percent'
|
||||
if spec_unit in {
|
||||
'weeks', 'days', 'hour', 'hours', 'minutes', 'seconds', 'ms', 'μs'
|
||||
}:
|
||||
if spec_unit in [
|
||||
'weeks', 'days', 'hours', 'minutes', 'seconds', 'ms', 'μs']:
|
||||
return 'mdi:clock'
|
||||
if spec_unit in {'celsius'}:
|
||||
if spec_unit in ['celsius']:
|
||||
return 'mdi:temperature-celsius'
|
||||
if spec_unit in {'fahrenheit'}:
|
||||
if spec_unit in ['fahrenheit']:
|
||||
return 'mdi:temperature-fahrenheit'
|
||||
if spec_unit in {'kelvin'}:
|
||||
if spec_unit in ['kelvin']:
|
||||
return 'mdi:temperature-kelvin'
|
||||
if spec_unit in {'μg/m3', 'mg/m3', 'ppm', 'ppb'}:
|
||||
if spec_unit in ['μg/m3', 'mg/m3', 'ppm', 'ppb']:
|
||||
return 'mdi:blur'
|
||||
if spec_unit in {'lux'}:
|
||||
if spec_unit in ['lux']:
|
||||
return 'mdi:brightness-6'
|
||||
if spec_unit in {'pascal', 'kilopascal', 'megapascal', 'mmHg', 'bar'}:
|
||||
if spec_unit in ['pascal', 'megapascal', 'bar']:
|
||||
return 'mdi:gauge'
|
||||
if spec_unit in {'watt', 'w', 'W'}:
|
||||
if spec_unit in ['watt']:
|
||||
return 'mdi:flash-triangle'
|
||||
if spec_unit in {'L', 'mL'}:
|
||||
if spec_unit in ['L', 'mL']:
|
||||
return 'mdi:gas-cylinder'
|
||||
if spec_unit in {'km/h', 'm/s'}:
|
||||
if spec_unit in ['km/h', 'm/s']:
|
||||
return 'mdi:speedometer'
|
||||
if spec_unit in {'kWh'}:
|
||||
if spec_unit in ['kWh']:
|
||||
return 'mdi:transmission-tower'
|
||||
if spec_unit in {'A', 'mA'}:
|
||||
if spec_unit in ['A', 'mA']:
|
||||
return 'mdi:current-ac'
|
||||
if spec_unit in {'V', 'mv', 'mV'}:
|
||||
if spec_unit in ['V', 'mV']:
|
||||
return 'mdi:current-dc'
|
||||
if spec_unit in {'cm', 'm', 'meter', 'km'}:
|
||||
if spec_unit in ['m', 'km']:
|
||||
return 'mdi:ruler'
|
||||
if spec_unit in {'rgb'}:
|
||||
if spec_unit in ['rgb']:
|
||||
return 'mdi:palette'
|
||||
if spec_unit in {'m3/h', 'L/s'}:
|
||||
if spec_unit in ['m3/h', 'L/s']:
|
||||
return 'mdi:pipe-leak'
|
||||
if spec_unit in {'μS/cm'}:
|
||||
if spec_unit in ['μS/cm']:
|
||||
return 'mdi:resistor-nodes'
|
||||
if spec_unit in {'gram', 'kilogram'}:
|
||||
if spec_unit in ['gram']:
|
||||
return 'mdi:weight'
|
||||
if spec_unit in {'dB'}:
|
||||
if spec_unit in ['dB']:
|
||||
return 'mdi:signal-distance-variant'
|
||||
if spec_unit in {'times'}:
|
||||
if spec_unit in ['times']:
|
||||
return 'mdi:counter'
|
||||
if spec_unit in {'mmol/L'}:
|
||||
if spec_unit in ['mmol/L']:
|
||||
return 'mdi:dots-hexagon'
|
||||
if spec_unit in {'kB', 'MB', 'GB'}:
|
||||
return 'mdi:network-pos'
|
||||
if spec_unit in {'arcdegress', 'arcdegrees'}:
|
||||
if spec_unit in ['arcdegress']:
|
||||
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'}:
|
||||
if spec_unit in ['kB']:
|
||||
return 'mdi:network-pos'
|
||||
if spec_unit in ['calorie', 'kCal']:
|
||||
return 'mdi:food'
|
||||
return None
|
||||
|
||||
def __gen_sub_id(self) -> int:
|
||||
self._sub_id += 1
|
||||
return self._sub_id
|
||||
|
||||
def __on_device_state_changed(
|
||||
self, did: str, state: MIoTDeviceState, ctx: Any
|
||||
) -> None:
|
||||
@ -1273,7 +1184,6 @@ class MIoTPropertyEntity(Entity):
|
||||
def __on_value_changed(self, params: dict, ctx: Any) -> None:
|
||||
_LOGGER.debug('property changed, %s', params)
|
||||
self._value = self.spec.value_format(params['value'])
|
||||
self._value = self.spec.eval_expr(self._value)
|
||||
if not self._pending_write_ha_state_timer:
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@ -1414,7 +1414,7 @@ class MipsLocalClient(_MipsClient):
|
||||
@final
|
||||
@on_dev_list_changed.setter
|
||||
def on_dev_list_changed(
|
||||
self, func: Optional[Callable[[Any, list[str]], Coroutine]]
|
||||
self, func: Callable[[Any, list[str]], Coroutine]
|
||||
) -> None:
|
||||
"""run in main loop."""
|
||||
self._on_dev_list_changed = func
|
||||
|
||||
@ -469,13 +469,14 @@ class _MIoTSpecBase:
|
||||
proprietary: bool
|
||||
need_filter: bool
|
||||
name: str
|
||||
icon: Optional[str]
|
||||
|
||||
# External params
|
||||
platform: Optional[str]
|
||||
device_class: Any
|
||||
state_class: Any
|
||||
icon: Optional[str]
|
||||
external_unit: Any
|
||||
expression: Optional[str]
|
||||
|
||||
spec_id: int
|
||||
|
||||
@ -488,12 +489,13 @@ class _MIoTSpecBase:
|
||||
self.proprietary = spec.get('proprietary', False)
|
||||
self.need_filter = spec.get('need_filter', False)
|
||||
self.name = spec.get('name', 'xiaomi')
|
||||
self.icon = spec.get('icon', None)
|
||||
|
||||
self.platform = None
|
||||
self.device_class = None
|
||||
self.state_class = None
|
||||
self.icon = None
|
||||
self.external_unit = None
|
||||
self.expression = None
|
||||
|
||||
self.spec_id = hash(f'{self.type_}.{self.iid}')
|
||||
|
||||
@ -508,7 +510,6 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
"""MIoT SPEC property class."""
|
||||
unit: Optional[str]
|
||||
precision: int
|
||||
expr: Optional[str]
|
||||
|
||||
_format_: Type
|
||||
_value_range: Optional[MIoTSpecValueRange]
|
||||
@ -530,8 +531,7 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
unit: Optional[str] = None,
|
||||
value_range: Optional[dict] = None,
|
||||
value_list: Optional[list[dict]] = None,
|
||||
precision: Optional[int] = None,
|
||||
expr: Optional[str] = None
|
||||
precision: Optional[int] = None
|
||||
) -> None:
|
||||
super().__init__(spec=spec)
|
||||
self.service = service
|
||||
@ -541,7 +541,6 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
self.value_range = value_range
|
||||
self.value_list = value_list
|
||||
self.precision = precision or 1
|
||||
self.expr = expr
|
||||
|
||||
self.spec_id = hash(
|
||||
f'p.{self.name}.{self.service.iid}.{self.iid}')
|
||||
@ -614,18 +613,6 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
elif isinstance(value, MIoTSpecValueList):
|
||||
self._value_list = value
|
||||
|
||||
def eval_expr(self, src_value: Any) -> Any:
|
||||
if not self.expr:
|
||||
return src_value
|
||||
try:
|
||||
# pylint: disable=eval-used
|
||||
return eval(self.expr, {'src_value': src_value})
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error(
|
||||
'eval expression error, %s, %s, %s, %s',
|
||||
self.iid, src_value, self.expr, err)
|
||||
return src_value
|
||||
|
||||
def value_format(self, value: Any) -> Any:
|
||||
if value is None:
|
||||
return None
|
||||
@ -652,9 +639,7 @@ class MIoTSpecProperty(_MIoTSpecBase):
|
||||
'value_range': (
|
||||
self._value_range.dump() if self._value_range else None),
|
||||
'value_list': self._value_list.dump() if self._value_list else None,
|
||||
'precision': self.precision,
|
||||
'expr': self.expr,
|
||||
'icon': self.icon
|
||||
'precision': self.precision
|
||||
}
|
||||
|
||||
|
||||
@ -747,6 +732,7 @@ class MIoTSpecService(_MIoTSpecBase):
|
||||
}
|
||||
|
||||
|
||||
# @dataclass
|
||||
class MIoTSpecInstance:
|
||||
"""MIoT SPEC instance class."""
|
||||
urn: str
|
||||
@ -788,8 +774,7 @@ class MIoTSpecInstance:
|
||||
unit=prop['unit'],
|
||||
value_range=prop['value_range'],
|
||||
value_list=prop['value_list'],
|
||||
precision=prop.get('precision', None),
|
||||
expr=prop.get('expr', None))
|
||||
precision=prop.get('precision', None))
|
||||
spec_service.properties.append(spec_prop)
|
||||
for event in service['events']:
|
||||
spec_event = MIoTSpecEvent(
|
||||
@ -1134,79 +1119,6 @@ class _SpecFilter:
|
||||
return False
|
||||
|
||||
|
||||
class _SpecModify:
|
||||
"""MIoT-Spec-V2 modify for entity conversion."""
|
||||
_SPEC_MODIFY_FILE = 'specs/spec_modify.yaml'
|
||||
_main_loop: asyncio.AbstractEventLoop
|
||||
_data: Optional[dict]
|
||||
_selected: Optional[dict]
|
||||
|
||||
def __init__(
|
||||
self, loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> None:
|
||||
self._main_loop = loop or asyncio.get_running_loop()
|
||||
self._data = None
|
||||
|
||||
async def init_async(self) -> None:
|
||||
if isinstance(self._data, dict):
|
||||
return
|
||||
modify_data = None
|
||||
self._data = {}
|
||||
self._selected = None
|
||||
try:
|
||||
modify_data = await self._main_loop.run_in_executor(
|
||||
None, load_yaml_file,
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
self._SPEC_MODIFY_FILE))
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error('spec modify, load file error, %s', err)
|
||||
return
|
||||
if not isinstance(modify_data, dict):
|
||||
_LOGGER.error('spec modify, invalid spec modify content')
|
||||
return
|
||||
for key, value in modify_data.items():
|
||||
if not isinstance(key, str) or not isinstance(value, (dict, str)):
|
||||
_LOGGER.error('spec modify, invalid spec modify data')
|
||||
return
|
||||
|
||||
self._data = modify_data
|
||||
|
||||
async def deinit_async(self) -> None:
|
||||
self._data = None
|
||||
self._selected = None
|
||||
|
||||
async def set_spec_async(self, urn: str) -> None:
|
||||
if not self._data:
|
||||
return
|
||||
self._selected = self._data.get(urn, None)
|
||||
if isinstance(self._selected, str):
|
||||
return await self.set_spec_async(urn=self._selected)
|
||||
|
||||
def get_prop_unit(self, siid: int, piid: int) -> Optional[str]:
|
||||
return self.__get_prop_item(siid=siid, piid=piid, key='unit')
|
||||
|
||||
def get_prop_expr(self, siid: int, piid: int) -> Optional[str]:
|
||||
return self.__get_prop_item(siid=siid, piid=piid, key='expr')
|
||||
|
||||
def get_prop_icon(self, siid: int, piid: int) -> Optional[str]:
|
||||
return self.__get_prop_item(siid=siid, piid=piid, key='icon')
|
||||
|
||||
def get_prop_access(self, siid: int, piid: int) -> Optional[list]:
|
||||
access = self.__get_prop_item(siid=siid, piid=piid, key='access')
|
||||
if not isinstance(access, list):
|
||||
return None
|
||||
return access
|
||||
|
||||
def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]:
|
||||
if not self._selected:
|
||||
return None
|
||||
prop = self._selected.get(f'prop.{siid}.{piid}', None)
|
||||
if not prop:
|
||||
return None
|
||||
return prop.get(key, None)
|
||||
|
||||
|
||||
class MIoTSpecParser:
|
||||
"""MIoT SPEC parser."""
|
||||
# pylint: disable=inconsistent-quotes
|
||||
@ -1220,7 +1132,6 @@ class MIoTSpecParser:
|
||||
_multi_lang: _MIoTSpecMultiLang
|
||||
_bool_trans: _SpecBoolTranslation
|
||||
_spec_filter: _SpecFilter
|
||||
_spec_modify: _SpecModify
|
||||
|
||||
_init_done: bool
|
||||
|
||||
@ -1238,7 +1149,6 @@ class MIoTSpecParser:
|
||||
self._bool_trans = _SpecBoolTranslation(
|
||||
lang=self._lang, loop=self._main_loop)
|
||||
self._spec_filter = _SpecFilter(loop=self._main_loop)
|
||||
self._spec_modify = _SpecModify(loop=self._main_loop)
|
||||
|
||||
self._init_done = False
|
||||
|
||||
@ -1247,7 +1157,6 @@ class MIoTSpecParser:
|
||||
return
|
||||
await self._bool_trans.init_async()
|
||||
await self._spec_filter.init_async()
|
||||
await self._spec_modify.init_async()
|
||||
std_lib_cache = await self._storage.load_async(
|
||||
domain=self._DOMAIN, name='spec_std_lib', type_=dict)
|
||||
if (
|
||||
@ -1287,7 +1196,6 @@ class MIoTSpecParser:
|
||||
# self._std_lib.deinit()
|
||||
await self._bool_trans.deinit_async()
|
||||
await self._spec_filter.deinit_async()
|
||||
await self._spec_modify.deinit_async()
|
||||
|
||||
async def parse(
|
||||
self, urn: str, skip_cache: bool = False,
|
||||
@ -1367,8 +1275,6 @@ class MIoTSpecParser:
|
||||
await self._multi_lang.set_spec_async(urn=urn)
|
||||
# Set spec filter
|
||||
await self._spec_filter.set_spec_spec(urn_key=urn_key)
|
||||
# Set spec modify
|
||||
await self._spec_modify.set_spec_async(urn=urn)
|
||||
# Parse device type
|
||||
spec_instance: MIoTSpecInstance = MIoTSpecInstance(
|
||||
urn=urn, name=urn_strs[3],
|
||||
@ -1414,14 +1320,12 @@ class MIoTSpecParser:
|
||||
):
|
||||
continue
|
||||
p_type_strs: list[str] = property_['type'].split(':')
|
||||
# Handle special property.unit
|
||||
unit = property_.get('unit', None)
|
||||
spec_prop: MIoTSpecProperty = MIoTSpecProperty(
|
||||
spec=property_,
|
||||
service=spec_service,
|
||||
format_=property_['format'],
|
||||
access=property_['access'],
|
||||
unit=unit if unit != 'none' else None)
|
||||
unit=property_.get('unit', None))
|
||||
spec_prop.name = p_type_strs[3]
|
||||
# Filter spec property
|
||||
spec_prop.need_filter = (
|
||||
@ -1461,19 +1365,7 @@ class MIoTSpecParser:
|
||||
if v_descriptions:
|
||||
# bool without value-list.name
|
||||
spec_prop.value_list = v_descriptions
|
||||
# Prop modify
|
||||
spec_prop.unit = self._spec_modify.get_prop_unit(
|
||||
siid=service['iid'], piid=property_['iid']
|
||||
) or spec_prop.unit
|
||||
spec_prop.expr = self._spec_modify.get_prop_expr(
|
||||
siid=service['iid'], piid=property_['iid'])
|
||||
spec_prop.icon = self._spec_modify.get_prop_icon(
|
||||
siid=service['iid'], piid=property_['iid'])
|
||||
spec_service.properties.append(spec_prop)
|
||||
custom_access = self._spec_modify.get_prop_access(
|
||||
siid=service['iid'], piid=property_['iid'])
|
||||
if custom_access:
|
||||
spec_prop.access = custom_access
|
||||
# Parse service event
|
||||
for event in service.get('events', []):
|
||||
if (
|
||||
|
||||
@ -59,43 +59,6 @@ data:
|
||||
urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no
|
||||
urn:miot-spec-v2:property:wind-reverse:00000117: yes_no
|
||||
translate:
|
||||
default:
|
||||
de:
|
||||
'false': Falsch
|
||||
'true': Wahr
|
||||
en:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
es:
|
||||
'false': Falso
|
||||
'true': Verdadero
|
||||
fr:
|
||||
'false': Faux
|
||||
'true': Vrai
|
||||
it:
|
||||
'false': Falso
|
||||
'true': Vero
|
||||
ja:
|
||||
'false': 偽
|
||||
'true': 真
|
||||
nl:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
pt:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
pt-BR:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
ru:
|
||||
'false': Ложь
|
||||
'true': Истина
|
||||
zh-Hans:
|
||||
'false': 假
|
||||
'true': 真
|
||||
zh-Hant:
|
||||
'false': 假
|
||||
'true': 真
|
||||
contact_state:
|
||||
de:
|
||||
'false': Kein Kontakt
|
||||
@ -133,6 +96,43 @@ translate:
|
||||
zh-Hant:
|
||||
'false': 分離
|
||||
'true': 接觸
|
||||
default:
|
||||
de:
|
||||
'false': Falsch
|
||||
'true': Wahr
|
||||
en:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
es:
|
||||
'false': Falso
|
||||
'true': Verdadero
|
||||
fr:
|
||||
'false': Faux
|
||||
'true': Vrai
|
||||
it:
|
||||
'false': Falso
|
||||
'true': Vero
|
||||
ja:
|
||||
'false': 偽
|
||||
'true': 真
|
||||
nl:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
pt:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
pt-BR:
|
||||
'false': 'False'
|
||||
'true': 'True'
|
||||
ru:
|
||||
'false': Ложь
|
||||
'true': Истина
|
||||
zh-Hans:
|
||||
'false': 假
|
||||
'true': 真
|
||||
zh-Hant:
|
||||
'false': 假
|
||||
'true': 真
|
||||
motion_state:
|
||||
de:
|
||||
'false': Keine Bewegung erkannt
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
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,15 +50,10 @@ from homeassistant.components.sensor import SensorStateClass
|
||||
from homeassistant.components.event import EventDeviceClass
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
LIGHT_LUX,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfTemperature,
|
||||
UnitOfPressure,
|
||||
PERCENTAGE
|
||||
)
|
||||
|
||||
# pylint: disable=pointless-string-statement
|
||||
@ -101,7 +96,7 @@ from homeassistant.const import (
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_DEVICE_TRANS_MAP: dict = {
|
||||
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'humidifier': {
|
||||
'required': {
|
||||
'humidifier': {
|
||||
@ -317,7 +312,7 @@ SPEC_DEVICE_TRANS_MAP: dict = {
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_SERVICE_TRANS_MAP: dict = {
|
||||
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
'light': {
|
||||
'required': {
|
||||
'properties': {
|
||||
@ -388,13 +383,15 @@ SPEC_SERVICE_TRANS_MAP: dict = {
|
||||
'<property instance name>':{
|
||||
'device_class': str,
|
||||
'entity': str,
|
||||
'state_class'?: str,
|
||||
'unit_of_measurement'?: str
|
||||
'optional':{
|
||||
'state_class': str,
|
||||
'unit_of_measurement': str
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
SPEC_PROP_TRANS_MAP: dict = {
|
||||
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
|
||||
'entities': {
|
||||
'sensor': {
|
||||
'format': {'int', 'float'},
|
||||
@ -408,111 +405,107 @@ SPEC_PROP_TRANS_MAP: dict = {
|
||||
'properties': {
|
||||
'temperature': {
|
||||
'device_class': SensorDeviceClass.TEMPERATURE,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfTemperature.CELSIUS
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'relative-humidity': {
|
||||
'device_class': SensorDeviceClass.HUMIDITY,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': PERCENTAGE
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'air-quality-index': {
|
||||
'device_class': SensorDeviceClass.AQI,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'pm2.5-density': {
|
||||
'device_class': SensorDeviceClass.PM25,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'pm10-density': {
|
||||
'device_class': SensorDeviceClass.PM10,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'pm1': {
|
||||
'device_class': SensorDeviceClass.PM1,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'atmospheric-pressure': {
|
||||
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPressure.PA
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'tvoc-density': {
|
||||
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'voc-density': 'tvoc-density',
|
||||
'battery-level': {
|
||||
'device_class': SensorDeviceClass.BATTERY,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': PERCENTAGE
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'voltage': {
|
||||
'device_class': SensorDeviceClass.VOLTAGE,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricPotential.VOLT
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricPotential.VOLT
|
||||
}
|
||||
},
|
||||
'electric-current': {
|
||||
'device_class': SensorDeviceClass.CURRENT,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfElectricCurrent.AMPERE
|
||||
}
|
||||
},
|
||||
'illumination': {
|
||||
'device_class': SensorDeviceClass.ILLUMINANCE,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': LIGHT_LUX
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'no-one-determine-time': {
|
||||
'device_class': SensorDeviceClass.DURATION,
|
||||
'entity': 'sensor'
|
||||
},
|
||||
'has-someone-duration': 'no-one-determine-time',
|
||||
'no-one-duration': 'no-one-determine-time',
|
||||
'electric-power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
},
|
||||
'surge-power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
},
|
||||
'power-consumption': {
|
||||
'device_class': SensorDeviceClass.ENERGY,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
},
|
||||
'power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
},
|
||||
'total-battery': {
|
||||
'device_class': SensorDeviceClass.ENERGY,
|
||||
'entity': 'sensor',
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.TOTAL_INCREASING,
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
},
|
||||
'has-someone-duration': 'no-one-determine-time',
|
||||
'no-one-duration': 'no-one-determine-time'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -95,7 +95,7 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||
# Set device_class
|
||||
if self._value_list:
|
||||
self._attr_device_class = SensorDeviceClass.ENUM
|
||||
self._attr_icon = 'mdi:format-text'
|
||||
self._attr_icon = 'mdi:message-text'
|
||||
self._attr_native_unit_of_measurement = None
|
||||
self._attr_options = self._value_list.descriptions
|
||||
else:
|
||||
@ -109,9 +109,6 @@ class Sensor(MIoTPropertyEntity, SensorEntity):
|
||||
self._attr_device_class, None) # type: ignore
|
||||
self._attr_native_unit_of_measurement = list(
|
||||
unit_sets)[0] if unit_sets else None
|
||||
# Set suggested precision
|
||||
if spec.format_ in {int, float}:
|
||||
self._attr_suggested_display_precision = spec.precision
|
||||
# Set state_class
|
||||
if spec.state_class:
|
||||
self._attr_state_class = spec.state_class
|
||||
|
||||
@ -98,8 +98,6 @@ footer :(可选)关联的 issue 或 pull request 编号。
|
||||
|
||||
在为本项目做出贡献时,您同意您的贡献遵循本项目的[许可证](../LICENSE.md) 。
|
||||
|
||||
当您第一次提交拉取请求时,GitHub Action 会提示您签署贡献者许可协议(Contributor License Agreement,CLA)。只有签署了 CLA ,本项目才会合入您的拉取请求。
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果您需要帮助或有疑问,可在 GitHub 的[讨论区](https://github.com/XiaoMi/ha_xiaomi_home/discussions/)询问。
|
||||
|
||||
@ -20,9 +20,6 @@ SPEC_BOOL_TRANS_FILE = path.join(
|
||||
SPEC_FILTER_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')
|
||||
SPEC_MODIFY_FILE = path.join(
|
||||
ROOT_PATH,
|
||||
'../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')
|
||||
|
||||
|
||||
def load_json_file(file_path: str) -> Optional[dict]:
|
||||
@ -57,8 +54,7 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
|
||||
def save_yaml_file(file_path: str, data: dict) -> None:
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
yaml.safe_dump(
|
||||
data, file, default_flow_style=False,
|
||||
allow_unicode=True, indent=2, sort_keys=False)
|
||||
data, file, default_flow_style=False, allow_unicode=True, indent=2)
|
||||
|
||||
|
||||
def dict_str_str(d: dict) -> bool:
|
||||
@ -139,21 +135,6 @@ def bool_trans(d: dict) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def spec_modify(data: dict) -> bool:
|
||||
"""dict[str, str | dict[str, dict]]"""
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
for urn, content in data.items():
|
||||
if not isinstance(urn, str) or not isinstance(content, (dict, str)):
|
||||
return False
|
||||
if isinstance(content, str):
|
||||
continue
|
||||
for key, value in content.items():
|
||||
if not isinstance(key, str) or not isinstance(value, dict):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
|
||||
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
|
||||
_LOGGER.info('invalid type')
|
||||
@ -200,12 +181,6 @@ def sort_spec_filter(file_path: str):
|
||||
return filter_data
|
||||
|
||||
|
||||
def sort_spec_modify(file_path: str):
|
||||
filter_data = load_yaml_file(file_path=file_path)
|
||||
assert isinstance(filter_data, dict), f'{file_path} format error'
|
||||
return dict(sorted(filter_data.items()))
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_bool_trans():
|
||||
data = load_yaml_file(SPEC_BOOL_TRANS_FILE)
|
||||
@ -222,14 +197,6 @@ def test_spec_filter():
|
||||
assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_spec_modify():
|
||||
data = load_yaml_file(SPEC_MODIFY_FILE)
|
||||
assert isinstance(data, dict)
|
||||
assert data, f'load {SPEC_MODIFY_FILE} failed'
|
||||
assert spec_modify(data), f'{SPEC_MODIFY_FILE} format error'
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_miot_i18n():
|
||||
for file_name in listdir(MIOT_I18N_RELATIVE_PATH):
|
||||
@ -319,6 +286,3 @@ def test_sort_spec_data():
|
||||
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
|
||||
save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)
|
||||
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
|
||||
sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE)
|
||||
save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data)
|
||||
_LOGGER.info('%s formatted.', SPEC_MODIFY_FILE)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user