Compare commits

...

6 Commits

Author SHA1 Message Date
Paul Shawn
7fd4b8017b
Merge d9d8433405 into 5d4b975f85 2025-01-07 12:25:24 +00:00
topsworld
d9d8433405 fix: fix type error 2025-01-07 20:24:06 +08:00
Paul Shawn
5d4b975f85
fix: the number of profile models updated from 660 to 823 (#583)
Some checks failed
Tests / check-rule-format (push) Has been cancelled
Validate / validate-hassfest (push) Has been cancelled
Validate / validate-hacs (push) Has been cancelled
Validate / validate-lint (push) Has been cancelled
Validate / validate-setup (push) Has been cancelled
2025-01-07 20:22:06 +08:00
Paul Shawn
0566546a99
feat: filter miwifi.* devices (#564)
* feat: filter miwifi.* devices

* feat: update log level

* feat: filter special xiaomi router model, xiaomi.router.rd03
2025-01-07 20:21:43 +08:00
Paul Shawn
c0d100ce2b
feat: fan entity support direction ctrl (#556)
* feat: fan entity support direction

* fix: fix value judgement logic
2025-01-07 20:21:24 +08:00
Li Shuzhen
ce7ce7af4b
fix: fan speed (#464)
* fix: fan speed

* fix: fan speed names map

* fix: set percentage

* docs: the instance code format of valuelist

* fix: fan level property

* fix: pylint too long line.

* style: code format

---------

Co-authored-by: topsworld <sworldtop@gmail.com>
2025-01-07 20:21:04 +08:00
8 changed files with 869 additions and 342 deletions

View File

@ -351,7 +351,7 @@ The instance code is the code of the MIoT-Spec-V2 instance, which is in the form
```
service:<siid> # service
service:<siid>:property:<piid> # property
service:<siid>:property:<piid>:valuelist:<value> # the value in value-list of a property
service:<siid>:property:<piid>:valuelist:<index> # The index of a value in the value-list of a property
service:<siid>:event:<eiid> # event
service:<siid>:action:<aiid> # action
```

View File

@ -55,7 +55,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.util.percentage import (
percentage_to_ranged_value,
ranged_value_to_percentage
ranged_value_to_percentage,
ordered_list_item_to_percentage,
percentage_to_ordered_list_item
)
from .miot.miot_spec import MIoTSpecProperty
@ -89,10 +91,15 @@ class Fan(MIoTServiceEntity, FanEntity):
_prop_fan_level: Optional[MIoTSpecProperty]
_prop_mode: Optional[MIoTSpecProperty]
_prop_horizontal_swing: Optional[MIoTSpecProperty]
_prop_wind_reverse: Optional[MIoTSpecProperty]
_prop_wind_reverse_forward: Any
_prop_wind_reverse_reverse: Any
_speed_min: Optional[int]
_speed_max: Optional[int]
_speed_step: Optional[int]
_speed_min: int
_speed_max: int
_speed_step: int
_speed_names: Optional[list]
_speed_name_map: Optional[dict[int, str]]
_mode_list: Optional[dict[Any, Any]]
def __init__(
@ -101,15 +108,22 @@ class Fan(MIoTServiceEntity, FanEntity):
"""Initialize the Fan."""
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._attr_preset_modes = []
self._attr_current_direction = None
self._attr_supported_features = FanEntityFeature(0)
self._prop_on = None
self._prop_fan_level = None
self._prop_mode = None
self._prop_horizontal_swing = None
self._prop_wind_reverse = None
self._prop_wind_reverse_forward = None
self._prop_wind_reverse_reverse = None
self._speed_min = 65535
self._speed_max = 0
self._speed_step = 1
self._speed_names = []
self._speed_name_map = {}
self._mode_list = None
# properties
@ -124,7 +138,8 @@ class Fan(MIoTServiceEntity, FanEntity):
self._speed_min = prop.value_range['min']
self._speed_max = prop.value_range['max']
self._speed_step = prop.value_range['step']
self._attr_speed_count = self._speed_max - self._speed_min+1
self._attr_speed_count = int((
self._speed_max - self._speed_min)/self._speed_step)+1
self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._prop_fan_level = prop
elif (
@ -133,10 +148,13 @@ class Fan(MIoTServiceEntity, FanEntity):
and prop.value_list
):
# Fan level with value-list
for item in prop.value_list:
self._speed_min = min(self._speed_min, item['value'])
self._speed_max = max(self._speed_max, item['value'])
self._attr_speed_count = self._speed_max - self._speed_min+1
# Fan level with value-range is prior to fan level with
# value-list when a fan has both fan level properties.
self._speed_name_map = {
item['value']: item['description']
for item in prop.value_list}
self._speed_names = list(self._speed_name_map.values())
self._attr_speed_count = len(prop.value_list)
self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._prop_fan_level = prop
elif prop.name == 'mode':
@ -156,6 +174,30 @@ class Fan(MIoTServiceEntity, FanEntity):
elif prop.name == 'horizontal-swing':
self._attr_supported_features |= FanEntityFeature.OSCILLATE
self._prop_horizontal_swing = prop
elif prop.name == 'wind-reverse':
if prop.format_ == 'bool':
self._prop_wind_reverse_forward = False
self._prop_wind_reverse_reverse = True
elif (
isinstance(prop.value_list, list)
and prop.value_list
):
for item in prop.value_list:
if item['name'].lower() in {'foreward'}:
self._prop_wind_reverse_forward = item['value']
elif item['name'].lower() in {
'reversal', 'reverse'}:
self._prop_wind_reverse_reverse = item['value']
if (
self._prop_wind_reverse_forward is None
or self._prop_wind_reverse_reverse is None
):
# NOTICE: Value may be 0 or False
_LOGGER.info(
'invalid wind-reverse, %s', self.entity_id)
continue
self._attr_supported_features |= FanEntityFeature.DIRECTION
self._prop_wind_reverse = prop
def __get_mode_description(self, key: int) -> Optional[str]:
if self._mode_list is None:
@ -182,9 +224,19 @@ class Fan(MIoTServiceEntity, FanEntity):
await self.set_property_async(prop=self._prop_on, value=True)
# percentage
if percentage:
await self.set_property_async(
prop=self._prop_fan_level,
value=int(percentage*self._attr_speed_count/100))
if self._speed_names:
speed = percentage_to_ordered_list_item(
self._speed_names, percentage)
speed_value = self.get_map_value(
map_=self._speed_name_map, description=speed)
await self.set_property_async(
prop=self._prop_fan_level, value=speed_value)
else:
await self.set_property_async(
prop=self._prop_fan_level,
value=int(percentage_to_ranged_value(
low_high_range=(self._speed_min, self._speed_max),
percentage=percentage)))
# preset_mode
if preset_mode:
await self.set_property_async(
@ -202,11 +254,19 @@ class Fan(MIoTServiceEntity, FanEntity):
async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage of the fan speed."""
if percentage > 0:
await self.set_property_async(
prop=self._prop_fan_level,
value=int(percentage_to_ranged_value(
low_high_range=(self._speed_min, self._speed_max),
percentage=percentage)))
if self._speed_names:
speed = percentage_to_ordered_list_item(
self._speed_names, percentage)
speed_value = self.get_map_value(
map_=self._speed_name_map, description=speed)
await self.set_property_async(
prop=self._prop_fan_level, value=speed_value)
else:
await self.set_property_async(
prop=self._prop_fan_level,
value=int(percentage_to_ranged_value(
low_high_range=(self._speed_min, self._speed_max),
percentage=percentage)))
if not self.is_on:
# If the fan is off, turn it on.
await self.set_property_async(prop=self._prop_on, value=True)
@ -221,6 +281,14 @@ class Fan(MIoTServiceEntity, FanEntity):
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
if not self._prop_wind_reverse:
return
await self.set_property_async(
prop=self._prop_wind_reverse,
value=(
self._prop_wind_reverse_reverse
if self.current_direction == 'reverse'
else self._prop_wind_reverse_forward))
async def async_oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan."""
@ -242,13 +310,28 @@ class Fan(MIoTServiceEntity, FanEntity):
key=self.get_prop_value(prop=self._prop_mode))
if self._prop_mode else None)
@property
def current_direction(self) -> Optional[str]:
"""Return the current direction of the fan."""
if not self._prop_wind_reverse:
return None
return 'reverse' if self.get_prop_value(
prop=self._prop_wind_reverse
) == self._prop_wind_reverse_reverse else 'forward'
@property
def percentage(self) -> Optional[int]:
"""Return the current percentage of the fan speed."""
fan_level = self.get_prop_value(prop=self._prop_fan_level)
return ranged_value_to_percentage(
low_high_range=(self._speed_min, self._speed_max),
value=fan_level) if fan_level else None
if fan_level is None:
return None
if self._speed_names:
return ordered_list_item_to_percentage(
self._speed_names, self._speed_name_map[fan_level])
else:
return ranged_value_to_percentage(
low_high_range=(self._speed_min, self._speed_max),
value=fan_level)
@property
def oscillating(self) -> Optional[bool]:
@ -257,8 +340,3 @@ class Fan(MIoTServiceEntity, FanEntity):
self.get_prop_value(
prop=self._prop_horizontal_swing)
if self._prop_horizontal_swing else None)
@property
def percentage_step(self) -> float:
"""Return the step of the fan speed."""
return self._speed_step

View File

@ -45,11 +45,14 @@ off Xiaomi or its affiliates' products.
Common utilities.
"""
import asyncio
import json
from os import path
import random
from typing import Any, Optional
import hashlib
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from paho.mqtt.matcher import MQTTMatcher
import yaml
@ -83,10 +86,12 @@ def randomize_int(value: int, ratio: float) -> int:
"""Randomize an integer value."""
return int(value * (1 - ratio + random.random()*2*ratio))
def randomize_float(value: float, ratio: float) -> float:
"""Randomize a float value."""
return value * (1 - ratio + random.random()*2*ratio)
class MIoTMatcher(MQTTMatcher):
"""MIoT Pub/Sub topic matcher."""
@ -105,3 +110,68 @@ class MIoTMatcher(MQTTMatcher):
return self[topic]
except KeyError:
return None
class MIoTHttp:
"""MIoT Common HTTP API."""
@staticmethod
def get(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[str]:
full_url = url
if params:
encoded_params = urlencode(params)
full_url = f'{url}?{encoded_params}'
request = Request(full_url, method='GET', headers=headers or {})
content: Optional[bytes] = None
with urlopen(request) as response:
content = response.read()
return str(content, 'utf-8') if content else None
@staticmethod
def get_json(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[dict]:
response = MIoTHttp.get(url, params, headers)
return json.loads(response) if response else None
@staticmethod
def post(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[str]:
pass
@staticmethod
def post_json(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None
) -> Optional[dict]:
response = MIoTHttp.post(url, data, headers)
return json.loads(response) if response else None
@staticmethod
async def get_async(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[str]:
# TODO: Use aiohttp
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.get, url, params, headers)
@staticmethod
async def get_json_async(
url: str, params: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[dict]:
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.get_json, url, params, headers)
@ staticmethod
async def post_async(
url: str, data: Optional[dict] = None, headers: Optional[dict] = None,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> Optional[str]:
ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor(
None, MIoTHttp.post, url, data, headers)

View File

@ -18,6 +18,10 @@
ts: 1603967572
1245.airpurifier.dl01:
ts: 1607502661
17216.magic_touch.d150:
ts: 1575097876
17216.magic_touch.d152:
ts: 1575097876
17216.massage.ec1266a:
ts: 1615881124
397.light.hallight:
@ -56,6 +60,10 @@ bj352.airmonitor.m30:
ts: 1686644541
bj352.waterpuri.s100cm:
ts: 1615795630
bymiot.gateway.v1:
ts: 1575097876
bymiot.gateway.v2:
ts: 1575097876
cgllc.airmonitor.b1:
ts: 1676339912
cgllc.airmonitor.s1:
@ -64,6 +72,8 @@ cgllc.clock.cgc1:
ts: 1686644422
cgllc.clock.dove:
ts: 1619607474
cgllc.gateway.s1:
ts: 1575097876
cgllc.magnet.hodor:
ts: 1724329476
cgllc.motion.cgpr1:
@ -120,8 +130,14 @@ chuangmi.cateye.ipc018:
ts: 1632735241
chuangmi.cateye.ipc508:
ts: 1633677521
chuangmi.door.hmi508:
ts: 1611733437
chuangmi.door.hmi515:
ts: 1640334316
chuangmi.gateway.ipc011:
ts: 1575097876
chuangmi.ir.v2:
ts: 1575097876
chuangmi.lock.hmi501:
ts: 1614742147
chuangmi.lock.hmi501b01:
@ -142,10 +158,18 @@ chuangmi.plug.v1:
ts: 1621925183
chuangmi.plug.v3:
ts: 1644480255
chuangmi.plug.vtl_v1:
ts: 1575097876
chuangmi.radio.v1:
ts: 1531108800
chuangmi.radio.v2:
ts: 1531108800
chuangmi.remote.h102a03:
ts: 1575097876
chuangmi.remote.h102c01:
ts: 1575097876
chuangmi.remote.v2:
ts: 1575097876
chunmi.cooker.eh1:
ts: 1607339278
chunmi.cooker.eh402:
@ -204,6 +228,8 @@ dmaker.airfresh.t2017:
ts: 1686731233
dmaker.fan.p5:
ts: 1655793784
doco.fcb.docov001:
ts: 1575097876
dsm.lock.h3:
ts: 1615283790
dsm.lock.q3:
@ -218,6 +244,30 @@ fawad.airrtc.fwd20011:
ts: 1610607149
fbs.airmonitor.pth02:
ts: 1686644918
fengmi.projector.fm05:
ts: 1575097876
fengmi.projector.fm15:
ts: 1575097876
fengmi.projector.fm154k:
ts: 1575097876
fengmi.projector.l166:
ts: 1650352923
fengmi.projector.l176:
ts: 1649936204
fengmi.projector.l246:
ts: 1575097876
fengmi.projector.m055:
ts: 1652839826
fengmi.projector.m055d:
ts: 1654067980
fengyu.intercom.beebird:
ts: 1575097876
fengyu.intercom.sharkv1:
ts: 1575097876
fotile.hood.emd1tmi:
ts: 1607483642
guoshi.other.sem01:
ts: 1602662080
hannto.printer.anise:
ts: 1618989537
hannto.printer.honey:
@ -226,14 +276,26 @@ hannto.printer.honey1s:
ts: 1614332725
hfjh.fishbowl.v1:
ts: 1615278556
hhcc.bleflowerpot.v2:
ts: 1575097876
hhcc.plantmonitor.v1:
ts: 1664163526
hith.foot_bath.q2:
ts: 1531108800
hmpace.bracelet.v4:
ts: 1575097876
hmpace.scales.mibfs:
ts: 1575097876
hmpace.scales.miscale2:
ts: 1575097876
huohe.lock.m1:
ts: 1635410938
huoman.litter_box.co1:
ts: 1687165034
hutlon.lock.v0001:
ts: 1634799698
idelan.aircondition.g1:
ts: 1575097876
idelan.aircondition.v1:
ts: 1614666973
idelan.aircondition.v2:
@ -248,14 +310,22 @@ ikea.light.led1537r6:
ts: 1605162872
ikea.light.led1545g12:
ts: 1605162937
ikea.light.led1546g12:
ts: 1575097876
ikea.light.led1623g12:
ts: 1605163009
ikea.light.led1649c5:
ts: 1605163064
ikea.light.led1650r5:
ts: 1575097876
imibar.cooker.mbihr3:
ts: 1624620659
imou99.camera.tp2:
ts: 1531108800
inovel.projector.me2:
ts: 1575097876
iracc.aircondition.d19:
ts: 1609914362
isa.camera.df3:
ts: 1531108800
isa.camera.hl5:
@ -266,18 +336,34 @@ isa.camera.isc5:
ts: 1531108800
isa.camera.isc5c1:
ts: 1621238175
isa.camera.qf3:
ts: 1575097876
isa.cateye.hldb6:
ts: 1575097876
isa.magnet.dw2hl:
ts: 1638274655
jieman.magic_touch.js78:
ts: 1575097876
jiqid.mistory.ipen1:
ts: 1575097876
jiqid.mistory.pro:
ts: 1531108800
jiqid.mistory.v1:
ts: 1531108800
jiqid.mistudy.v2:
ts: 1610612349
jiqid.robot.cube:
ts: 1575097876
jiwu.lock.jwp01:
ts: 1614752632
jyaiot.cm.ccj01:
ts: 1611824545
k0918.toothbrush.kid01:
ts: 1575097876
kejia.airer.th001:
ts: 1575097876
ksmb.treadmill.k12:
ts: 1575097876
ksmb.treadmill.v1:
ts: 1611211447
ksmb.treadmill.v2:
@ -390,6 +476,8 @@ loock.lock.xfvl10:
ts: 1632814256
loock.safe.v1:
ts: 1619607755
lumi.acpartner.mcn02:
ts: 1655791626
lumi.acpartner.v1:
ts: 1531108800
lumi.acpartner.v2:
@ -462,6 +550,8 @@ lumi.lock.acn02:
ts: 1623928631
lumi.lock.acn03:
ts: 1614752574
lumi.lock.aq1:
ts: 1612518044
lumi.lock.bacn01:
ts: 1614741699
lumi.lock.bmcn02:
@ -482,6 +572,8 @@ lumi.lock.mcn007:
ts: 1650446757
lumi.lock.mcn01:
ts: 1679881881
lumi.lock.v1:
ts: 1575097876
lumi.lock.wbmcn1:
ts: 1619422072
lumi.motion.bmgl01:
@ -510,14 +602,20 @@ lumi.sensor_86sw1.v1:
ts: 1609311038
lumi.sensor_86sw2.v1:
ts: 1608795035
lumi.sensor_cube.aqgl01:
ts: 1575097876
lumi.sensor_ht.v1:
ts: 1621239877
lumi.sensor_magnet.aq2:
ts: 1641112867
lumi.sensor_magnet.v1:
ts: 1606120416
lumi.sensor_magnet.v2:
ts: 1641113779
lumi.sensor_motion.aq2:
ts: 1676433994
lumi.sensor_motion.v1:
ts: 1605093075
lumi.sensor_motion.v2:
ts: 1672818550
lumi.sensor_natgas.v1:
@ -530,6 +628,8 @@ lumi.sensor_switch.aq2:
ts: 1615256430
lumi.sensor_switch.aq3:
ts: 1607399487
lumi.sensor_switch.v1:
ts: 1606874434
lumi.sensor_switch.v2:
ts: 1609310683
lumi.sensor_wleak.aq1:
@ -574,6 +674,20 @@ miaomiaoce.sensor_ht.t1:
ts: 1616057242
miaomiaoce.sensor_ht.t2:
ts: 1636603553
miaomiaoce.thermo.t01:
ts: 1575097876
midea.aircondition.v1:
ts: 1575097876
midea.aircondition.xa1:
ts: 1575097876
midea.aircondition.xa2:
ts: 1575097876
midr.rv_mirror.m2:
ts: 1575097876
midr.rv_mirror.m5:
ts: 1575097876
midr.rv_mirror.v1:
ts: 1575097876
miir.aircondition.ir01:
ts: 1531108800
miir.aircondition.ir02:
@ -612,6 +726,8 @@ minij.washer.v5:
ts: 1622792196
minij.washer.v8:
ts: 1615777868
minuo.tracker.lm001:
ts: 1575097876
miot.light.plato2:
ts: 1685518142
miot.light.plato3:
@ -624,18 +740,32 @@ mmgg.feeder.snack:
ts: 1607503182
moyu.washer.s1hm:
ts: 1624620888
mrbond.airer.m0:
ts: 1575097876
mrbond.airer.m1pro:
ts: 1646393746
mrbond.airer.m1s:
ts: 1646393874
mrbond.airer.m1super:
ts: 1575097876
msj.f_washer.m1:
ts: 1614914340
mxiang.cateye.mdb10:
ts: 1616140362
mxiang.cateye.xmcatt1:
ts: 1616140207
nhy.airrtc.v1:
ts: 1575097876
ninebot.scooter.v1:
ts: 1602662395
ninebot.scooter.v6:
ts: 1575097876
nuwa.robot.minikiwi:
ts: 1575097876
nwt.derh.wdh318efw1:
ts: 1611822375
onemore.wifispeaker.sm4:
ts: 1575097876
opple.light.bydceiling:
ts: 1608187619
opple.light.fanlight:
@ -646,6 +776,8 @@ opple.remote.5pb112:
ts: 1627453840
opple.remote.5pb113:
ts: 1636599905
orion.wifispeaker.cm1:
ts: 1575097876
ows.towel_w.mj1x0:
ts: 1610604939
philips.light.bceiling1:
@ -696,6 +828,8 @@ pwzn.relay.apple:
ts: 1611217196
pwzn.relay.banana:
ts: 1646647255
qicyc.bike.tdp02z:
ts: 1575097876
qike.bhf_light.qk201801:
ts: 1608174909
qmi.powerstrip.v1:
@ -726,8 +860,32 @@ roborock.vacuum.t6:
ts: 1619423841
rockrobo.vacuum.v1:
ts: 1531108800
roidmi.carairpuri.pro:
ts: 1575097876
roidmi.carairpuri.v1:
ts: 1575097876
roidmi.cleaner.f8pro:
ts: 1575097876
roidmi.cleaner.v1:
ts: 1575097876
roidmi.cleaner.v2:
ts: 1638514177
roidmi.cleaner.v382:
ts: 1575097876
roidmi.vacuum.v1:
ts: 1575097876
rokid.robot.me:
ts: 1575097876
rokid.robot.mini:
ts: 1575097876
rokid.robot.pebble:
ts: 1575097876
rokid.robot.pebble2:
ts: 1575097876
roome.bhf_light.yf6002:
ts: 1531108800
rotai.magic_touch.sx300:
ts: 1602662578
rotai.massage.rt5728:
ts: 1610607000
rotai.massage.rt5850:
@ -738,22 +896,42 @@ rotai.massage.rt5863:
ts: 1611827937
rotai.massage.rt5870:
ts: 1632376570
runmi.suitcase.v1:
ts: 1575097876
scishare.coffee.s1102:
ts: 1611824402
shjszn.gateway.c1:
ts: 1575097876
shjszn.lock.c1:
ts: 1575097876
shjszn.lock.kx:
ts: 1575097876
shuii.humidifier.jsq001:
ts: 1575097876
shuii.humidifier.jsq002:
ts: 1606376290
skyrc.feeder.dfeed:
ts: 1626082349
skyrc.pet_waterer.fre1:
ts: 1608186812
smith.w_soften.cxs05ta1:
ts: 1575097876
smith.waterheater.cxea1:
ts: 1611826349
smith.waterheater.cxeb1:
ts: 1611826388
smith.waterpuri.jnt600:
ts: 1531108800
soocare.toothbrush.m1:
ts: 1575097876
soocare.toothbrush.m1s:
ts: 1610611310
soocare.toothbrush.mc1:
ts: 1575097876
soocare.toothbrush.t501:
ts: 1672192586
soocare.toothbrush.x3:
ts: 1575097876
sxds.pillow.pillow02:
ts: 1611222235
syniot.curtain.syc1:
@ -778,6 +956,10 @@ tokit.oven.tk32pro1:
ts: 1617002408
tokit.pre_cooker.tkih1:
ts: 1607410832
trios1.bleshoes.v02:
ts: 1602662599
txdd.wifispeaker.x1:
ts: 1575097876
viomi.aircondition.v10:
ts: 1606375041
viomi.aircondition.v21:
@ -830,12 +1012,16 @@ viomi.fridge.u13:
ts: 1614667152
viomi.fridge.u15:
ts: 1607505693
viomi.fridge.u17:
ts: 1575097876
viomi.fridge.u18:
ts: 1614655755
viomi.fridge.u2:
ts: 1531108800
viomi.fridge.u24:
ts: 1614667214
viomi.fridge.u25:
ts: 1575097876
viomi.fridge.u4:
ts: 1614667295
viomi.fridge.u6:
@ -992,6 +1178,82 @@ xiaomi.aircondition.ma6:
ts: 1721629272
xiaomi.aircondition.ma9:
ts: 1721629362
xiaomi.plc.v1:
ts: 1575097876
xiaomi.repeater.v1:
ts: 1575097876
xiaomi.repeater.v2:
ts: 1575097876
xiaomi.repeater.v3:
ts: 1575097876
xiaomi.router.d01:
ts: 1575097876
xiaomi.router.lv1:
ts: 1575097876
xiaomi.router.lv3:
ts: 1575097876
xiaomi.router.mv1:
ts: 1575097876
xiaomi.router.r2100:
ts: 1575097876
xiaomi.router.r3600:
ts: 1575097876
xiaomi.router.r3a:
ts: 1575097876
xiaomi.router.r3d:
ts: 1575097876
xiaomi.router.r3g:
ts: 1575097876
xiaomi.router.r3gv2:
ts: 1575097876
xiaomi.router.r3gv2n:
ts: 1575097876
xiaomi.router.r3p:
ts: 1575097876
xiaomi.router.r4:
ts: 1575097876
xiaomi.router.r4a:
ts: 1575097876
xiaomi.router.r4ac:
ts: 1575097876
xiaomi.router.r4c:
ts: 1575097876
xiaomi.router.r4cm:
ts: 1575097876
xiaomi.router.rm1800:
ts: 1575097876
xiaomi.router.v1:
ts: 1575097876
xiaomi.router.v2:
ts: 1575097876
xiaomi.router.v3:
ts: 1575097876
xiaomi.split_tv.b1:
ts: 1575097876
xiaomi.split_tv.v1:
ts: 1575097876
xiaomi.tv.b1:
ts: 1661248580
xiaomi.tv.h1:
ts: 1575097876
xiaomi.tv.i1:
ts: 1661248572
xiaomi.tv.v1:
ts: 1670811870
xiaomi.tvbox.b1:
ts: 1694503508
xiaomi.tvbox.i1:
ts: 1694503515
xiaomi.tvbox.v1:
ts: 1694503501
xiaomi.watch.band1:
ts: 1575097876
xiaomi.watch.band1A:
ts: 1575097876
xiaomi.watch.band1S:
ts: 1575097876
xiaomi.watch.band2:
ts: 1575097876
xiaomi.wifispeaker.l04m:
ts: 1658817956
xiaomi.wifispeaker.l06a:
@ -1012,6 +1274,10 @@ xiaomi.wifispeaker.lx5a:
ts: 1672299577
xiaomi.wifispeaker.s12:
ts: 1672299594
xiaomi.wifispeaker.v1:
ts: 1575097876
xiaomi.wifispeaker.v3:
ts: 1575097876
xiaomi.wifispeaker.x08a:
ts: 1672818945
xiaomi.wifispeaker.x08c:
@ -1028,6 +1294,44 @@ xiaovv.camera.xvd5:
ts: 1531108800
xiaovv.camera.xvsnowman:
ts: 1531108800
xiaoxun.robot.v1:
ts: 1575097876
xiaoxun.tracker.v1:
ts: 1575097876
xiaoxun.watch.sw306:
ts: 1575097876
xiaoxun.watch.sw560:
ts: 1575097876
xiaoxun.watch.sw705:
ts: 1575097876
xiaoxun.watch.sw710a2:
ts: 1575097876
xiaoxun.watch.sw760:
ts: 1575097876
xiaoxun.watch.sw900:
ts: 1575097876
xiaoxun.watch.sw960:
ts: 1575097876
xiaoxun.watch.v1:
ts: 1575097876
xiaoxun.watch.v10:
ts: 1575097876
xiaoxun.watch.v11:
ts: 1575097876
xiaoxun.watch.v2:
ts: 1575097876
xiaoxun.watch.v3:
ts: 1575097876
xiaoxun.watch.v4:
ts: 1575097876
xiaoxun.watch.v5:
ts: 1575097876
xiaoxun.watch.v7:
ts: 1575097876
xiaoxun.watch.v8:
ts: 1575097876
xiaoxun.watch.v9:
ts: 1575097876
xjx.toilet.pro:
ts: 1615965466
xjx.toilet.pure:
@ -1054,6 +1358,8 @@ yeelink.bhf_light.v3:
ts: 1608790102
yeelink.bhf_light.v5:
ts: 1601292562
yeelink.gateway.v1:
ts: 1575097876
yeelink.light.bslamp1:
ts: 1703120679
yeelink.light.bslamp2:
@ -1192,6 +1498,10 @@ yunmi.kettle.r2:
ts: 1606372087
yunmi.kettle.r3:
ts: 1637309534
yunmi.kettle.v1:
ts: 1575097876
yunmi.kettle.v9:
ts: 1602662686
yunmi.plmachine.mg2:
ts: 1611833658
yunmi.waterpuri.c5:
@ -1230,18 +1540,26 @@ yunmi.waterpurifier.v2:
ts: 1632377061
yunmi.waterpurifier.v3:
ts: 1611221428
yunyi.camera.v1:
ts: 1575097876
yyunyi.wopener.yypy24:
ts: 1616741966
yyzhn.gateway.yn181126:
ts: 1610689325
zdeer.ajh.a8:
ts: 1531108800
zdeer.ajh.a9:
ts: 1531108800
zdeer.ajh.ajb:
ts: 1608276454
zdeer.ajh.zda10:
ts: 1531108800
zdeer.ajh.zda9:
ts: 1531108800
zdeer.ajh.zjy:
ts: 1531108800
zhij.toothbrush.bv1:
ts: 1575097876
zhimi.aircondition.ma1:
ts: 1615185265
zhimi.aircondition.ma3:
@ -1250,6 +1568,8 @@ zhimi.aircondition.ma4:
ts: 1626334057
zhimi.aircondition.v1:
ts: 1610610931
zhimi.aircondition.v2:
ts: 1575097876
zhimi.aircondition.va1:
ts: 1609924720
zhimi.aircondition.za1:
@ -1276,8 +1596,12 @@ zhimi.airpurifier.sa2:
ts: 1635820002
zhimi.airpurifier.v1:
ts: 1635855633
zhimi.airpurifier.v2:
ts: 1575097876
zhimi.airpurifier.v3:
ts: 1676339933
zhimi.airpurifier.v5:
ts: 1575097876
zhimi.airpurifier.v6:
ts: 1636978652
zhimi.airpurifier.v7:
@ -1318,3 +1642,5 @@ zimi.mosq.v1:
ts: 1620728957
zimi.powerstrip.v2:
ts: 1620812714
zimi.projector.v1:
ts: 1575097876

View File

@ -531,9 +531,18 @@ class MIoTHttpClient:
name = device.get('name', None)
urn = device.get('spec_type', None)
model = device.get('model', None)
if did is None or name is None or urn is None or model is None:
_LOGGER.error(
'get_device_list, cloud, invalid device, %s', device)
if did is None or name is None:
_LOGGER.info(
'invalid device, cloud, %s', device)
continue
if urn is None or model is None:
_LOGGER.info(
'missing the urn|model field, cloud, %s', device)
continue
if did.startswith('miwifi.'):
# The miwifi.* routers defined SPEC functions, but none of them
# were implemented.
_LOGGER.info('ignore miwifi.* device, cloud, %s', did)
continue
device_infos[did] = {
'did': did,
@ -634,7 +643,7 @@ class MIoTHttpClient:
for did in dids:
if did not in results:
devices.pop(did, None)
_LOGGER.error('get device info failed, %s', did)
_LOGGER.info('get device info failed, %s', did)
continue
devices[did].update(results[did])
# Whether sub devices

View File

@ -54,8 +54,10 @@ from urllib.parse import urlencode
from urllib.request import Request, urlopen
import logging
# pylint: disable=relative-beyond-top-level
from .const import DEFAULT_INTEGRATION_LANGUAGE, SPEC_STD_LIB_EFFECTIVE_TIME
from .common import MIoTHttp
from .miot_error import MIoTSpecError
from .miot_storage import (
MIoTStorage,
@ -66,6 +68,291 @@ from .miot_storage import (
_LOGGER = logging.getLogger(__name__)
class _MIoTSpecValueRange:
"""MIoT SPEC value range class."""
min_: int
max_: int
step: int
def from_list(self, value_range: list) -> None:
self.min_ = value_range[0]
self.max_ = value_range[1]
self.step = value_range[2]
def to_list(self) -> list:
return [self.min_, self.max_, self.step]
class _MIoTSpecValueListItem:
"""MIoT SPEC value list item class."""
# All lower-case SPEC description.
name: str
# Value
value: Any
# Descriptions after multilingual conversion.
description: str
def to_dict(self) -> dict:
return {
'name': self.name,
'value': self.value,
'description': self.description
}
class _MIoTSpecValueList:
"""MIoT SPEC value list class."""
items: list[_MIoTSpecValueListItem]
def to_map(self) -> dict:
return {item.value: item.description for item in self.items}
def to_list(self) -> list:
return [item.to_dict() for item in self.items]
class _SpecStdLib:
"""MIoT-Spec-V2 standard library."""
_lang: str
_devices: dict[str, dict[str, str]]
_services: dict[str, dict[str, str]]
_properties: dict[str, dict[str, str]]
_events: dict[str, dict[str, str]]
_actions: dict[str, dict[str, str]]
_values: dict[str, dict[str, str]]
def __init__(self, lang: str) -> None:
self._lang = lang
self._devices = {}
self._services = {}
self._properties = {}
self._events = {}
self._actions = {}
self._values = {}
self._spec_std_lib = None
def from_dict(self, std_lib: dict[str, dict[str, dict[str, str]]]) -> None:
if (
not isinstance(std_lib, dict)
or 'devices' not in std_lib
or 'services' not in std_lib
or 'properties' not in std_lib
or 'events' not in std_lib
or 'actions' not in std_lib
or 'values' not in std_lib
):
return
self._devices = std_lib['devices']
self._services = std_lib['services']
self._properties = std_lib['properties']
self._events = std_lib['events']
self._actions = std_lib['actions']
self._values = std_lib['values']
def device_translate(self, key: str) -> Optional[str]:
if not self._devices or key not in self._devices:
return None
if self._lang not in self._devices[key]:
return self._devices[key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._devices[key][self._lang]
def service_translate(self, key: str) -> Optional[str]:
if not self._services or key not in self._services:
return None
if self._lang not in self._services[key]:
return self._services[key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._services[key][self._lang]
def property_translate(self, key: str) -> Optional[str]:
if not self._properties or key not in self._properties:
return None
if self._lang not in self._properties[key]:
return self._properties[key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._properties[key][self._lang]
def event_translate(self, key: str) -> Optional[str]:
if not self._events or key not in self._events:
return None
if self._lang not in self._events[key]:
return self._events[key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._events[key][self._lang]
def action_translate(self, key: str) -> Optional[str]:
if not self._actions or key not in self._actions:
return None
if self._lang not in self._actions[key]:
return self._actions[key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._actions[key][self._lang]
def value_translate(self, key: str) -> Optional[str]:
if not self._values or key not in self._values:
return None
if self._lang not in self._values[key]:
return self._values[key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._values[key][self._lang]
def dump(self) -> dict[str, dict[str, dict[str, str]]]:
return {
'devices': self._devices,
'services': self._services,
'properties': self._properties,
'events': self._events,
'actions': self._actions,
'values': self._values
}
async def refresh_async(self) -> bool:
std_lib_new = await self.__request_from_cloud_async()
if std_lib_new:
self.from_dict(std_lib_new)
return True
return False
async def __request_from_cloud_async(self) -> Optional[dict]:
std_libs: Optional[dict] = None
for index in range(3):
try:
tasks: list = []
# Get std lib
for name in [
'device', 'service', 'property', 'event', 'action']:
tasks.append(self.__get_template_list(
'https://miot-spec.org/miot-spec-v2/template/list/'
+ name))
tasks.append(self.__get_property_value())
# Async request
results = await asyncio.gather(*tasks)
if None in results:
raise MIoTSpecError('init failed, None in result')
std_libs = {
'devices': results[0],
'services': results[1],
'properties': results[2],
'events': results[3],
'actions': results[4],
'values': results[5],
}
# Get external std lib, Power by LM
tasks.clear()
for name in [
'device', 'service', 'property', 'event', 'action',
'property_value']:
tasks.append(MIoTHttp.get_json_async(
'https://cdn.cnbj1.fds.api.mi-img.com/res-conf/'
f'xiaomi-home/std_ex_{name}.json'))
results = await asyncio.gather(*tasks)
if results[0]:
for key, value in results[0].items():
if key in std_libs['devices']:
std_libs['devices'][key].update(value)
else:
std_libs['devices'][key] = value
else:
_LOGGER.error('get external std lib failed, devices')
if results[1]:
for key, value in results[1].items():
if key in std_libs['services']:
std_libs['services'][key].update(value)
else:
std_libs['services'][key] = value
else:
_LOGGER.error('get external std lib failed, services')
if results[2]:
for key, value in results[2].items():
if key in std_libs['properties']:
std_libs['properties'][key].update(value)
else:
std_libs['properties'][key] = value
else:
_LOGGER.error('get external std lib failed, properties')
if results[3]:
for key, value in results[3].items():
if key in std_libs['events']:
std_libs['events'][key].update(value)
else:
std_libs['events'][key] = value
else:
_LOGGER.error('get external std lib failed, events')
if results[4]:
for key, value in results[4].items():
if key in std_libs['actions']:
std_libs['actions'][key].update(value)
else:
std_libs['actions'][key] = value
else:
_LOGGER.error('get external std lib failed, actions')
if results[5]:
for key, value in results[5].items():
if key in std_libs['values']:
std_libs['values'][key].update(value)
else:
std_libs['values'][key] = value
else:
_LOGGER.error(
'get external std lib failed, values')
return std_libs
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error(
'update spec std lib error, retry, %d, %s', index, err)
return None
async def __get_property_value(self) -> dict:
reply = await MIoTHttp.get_json_async(
url='https://miot-spec.org/miot-spec-v2'
'/normalization/list/property_value')
if reply is None or 'result' not in reply:
raise MIoTSpecError('get property value failed')
result = {}
for item in reply['result']:
if (
not isinstance(item, dict)
or 'normalization' not in item
or 'description' not in item
or 'proName' not in item
or 'urn' not in item
):
continue
result[
f'{item["urn"]}|{item["proName"]}|{item["normalization"]}'
] = {
'zh-Hans': item['description'],
'en': item['normalization']
}
return result
async def __get_template_list(self, url: str) -> dict:
reply = await MIoTHttp.get_json_async(url=url)
if reply is None or 'result' not in reply:
raise MIoTSpecError(f'get service failed, {url}')
result: dict = {}
for item in reply['result']:
if (
not isinstance(item, dict)
or 'type' not in item
or 'description' not in item
):
continue
if 'zh_cn' in item['description']:
item['description']['zh-Hans'] = item['description'].pop(
'zh_cn')
if 'zh_hk' in item['description']:
item['description']['zh-Hant'] = item['description'].pop(
'zh_hk')
item['description'].pop('zh_tw', None)
elif 'zh_tw' in item['description']:
item['description']['zh-Hant'] = item['description'].pop(
'zh_tw')
result[item['type']] = item['description']
return result
class MIoTSpecBase:
"""MIoT SPEC base class."""
iid: int
@ -77,13 +364,13 @@ class MIoTSpecBase:
name: Optional[str]
# External params
platform: str
platform: Optional[str]
device_class: Any
state_class: Any
icon: str
icon: Optional[str]
external_unit: Any
spec_id: str
spec_id: int
def __init__(self, spec: dict) -> None:
self.iid = spec['iid']
@ -106,7 +393,7 @@ class MIoTSpecBase:
def __hash__(self) -> int:
return self.spec_id
def __eq__(self, value: object) -> bool:
def __eq__(self, value) -> bool:
return self.spec_id == value.spec_id
@ -114,10 +401,10 @@ class MIoTSpecProperty(MIoTSpecBase):
"""MIoT SPEC property class."""
format_: str
precision: int
unit: str
unit: Optional[str]
value_range: list
value_list: list[dict]
value_range: Optional[list]
value_list: Optional[list[dict]]
_access: list
_writable: bool
@ -127,10 +414,9 @@ class MIoTSpecProperty(MIoTSpecBase):
service: MIoTSpecBase
def __init__(
self, spec: dict, service: MIoTSpecBase = None,
format_: str = None, access: list = None,
unit: str = None, value_range: list = None,
value_list: list[dict] = None, precision: int = 0
self, spec: dict, service: MIoTSpecBase, format_: str, access: list,
unit: Optional[str] = None, value_range: Optional[list] = None,
value_list: Optional[list[dict]] = None, precision: int = 0
) -> None:
super().__init__(spec=spec)
self.service = service
@ -203,7 +489,7 @@ class MIoTSpecEvent(MIoTSpecBase):
service: MIoTSpecBase
def __init__(
self, spec: dict, service: MIoTSpecBase = None,
self, spec: dict, service: MIoTSpecBase,
argument: list[MIoTSpecProperty] = None
) -> None:
super().__init__(spec=spec)
@ -372,86 +658,6 @@ class MIoTSpecInstance:
}
class SpecStdLib:
"""MIoT-Spec-V2 standard library."""
_lang: str
_spec_std_lib: Optional[dict[str, dict[str, dict[str, str]]]]
def __init__(self, lang: str) -> None:
self._lang = lang
self._spec_std_lib = None
def init(self, std_lib: dict[str, dict[str, str]]) -> None:
if (
not isinstance(std_lib, dict)
or 'devices' not in std_lib
or 'services' not in std_lib
or 'properties' not in std_lib
or 'events' not in std_lib
or 'actions' not in std_lib
or 'values' not in std_lib
):
return
self._spec_std_lib = std_lib
def deinit(self) -> None:
self._spec_std_lib = None
def device_translate(self, key: str) -> Optional[str]:
if not self._spec_std_lib or key not in self._spec_std_lib['devices']:
return None
if self._lang not in self._spec_std_lib['devices'][key]:
return self._spec_std_lib['devices'][key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._spec_std_lib['devices'][key][self._lang]
def service_translate(self, key: str) -> Optional[str]:
if not self._spec_std_lib or key not in self._spec_std_lib['services']:
return None
if self._lang not in self._spec_std_lib['services'][key]:
return self._spec_std_lib['services'][key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._spec_std_lib['services'][key][self._lang]
def property_translate(self, key: str) -> Optional[str]:
if (
not self._spec_std_lib
or key not in self._spec_std_lib['properties']
):
return None
if self._lang not in self._spec_std_lib['properties'][key]:
return self._spec_std_lib['properties'][key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._spec_std_lib['properties'][key][self._lang]
def event_translate(self, key: str) -> Optional[str]:
if not self._spec_std_lib or key not in self._spec_std_lib['events']:
return None
if self._lang not in self._spec_std_lib['events'][key]:
return self._spec_std_lib['events'][key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._spec_std_lib['events'][key][self._lang]
def action_translate(self, key: str) -> Optional[str]:
if not self._spec_std_lib or key not in self._spec_std_lib['actions']:
return None
if self._lang not in self._spec_std_lib['actions'][key]:
return self._spec_std_lib['actions'][key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._spec_std_lib['actions'][key][self._lang]
def value_translate(self, key: str) -> Optional[str]:
if not self._spec_std_lib or key not in self._spec_std_lib['values']:
return None
if self._lang not in self._spec_std_lib['values'][key]:
return self._spec_std_lib['values'][key].get(
DEFAULT_INTEGRATION_LANGUAGE, None)
return self._spec_std_lib['values'][key][self._lang]
def dump(self) -> dict[str, dict[str, str]]:
return self._spec_std_lib
class MIoTSpecParser:
"""MIoT SPEC parser."""
# pylint: disable=inconsistent-quotes
@ -464,24 +670,24 @@ class MIoTSpecParser:
_init_done: bool
_ram_cache: dict
_std_lib: SpecStdLib
_std_lib: _SpecStdLib
_bool_trans: SpecBoolTranslation
_multi_lang: SpecMultiLang
_spec_filter: SpecFilter
def __init__(
self, lang: str = DEFAULT_INTEGRATION_LANGUAGE,
storage: MIoTStorage = None,
self, lang: Optional[str],
storage: MIoTStorage,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._lang = lang
self._lang = lang or DEFAULT_INTEGRATION_LANGUAGE
self._storage = storage
self._main_loop = loop or asyncio.get_running_loop()
self._init_done = False
self._ram_cache = {}
self._std_lib = SpecStdLib(lang=self._lang)
self._std_lib = _SpecStdLib(lang=self._lang)
self._bool_trans = SpecBoolTranslation(
lang=self._lang, loop=self._main_loop)
self._multi_lang = SpecMultiLang(lang=self._lang, loop=self._main_loop)
@ -493,48 +699,43 @@ class MIoTSpecParser:
await self._bool_trans.init_async()
await self._multi_lang.init_async()
await self._spec_filter.init_async()
std_lib_cache: dict = None
if self._storage:
std_lib_cache: dict = await self._storage.load_async(
domain=self.DOMAIN, name='spec_std_lib', type_=dict)
if (
isinstance(std_lib_cache, dict)
and 'data' in std_lib_cache
and 'ts' in std_lib_cache
and isinstance(std_lib_cache['ts'], int)
and int(time.time()) - std_lib_cache['ts'] <
SPEC_STD_LIB_EFFECTIVE_TIME
):
# Use the cache if the update time is less than 14 day
_LOGGER.debug(
'use local spec std cache, ts->%s', std_lib_cache['ts'])
self._std_lib.init(std_lib_cache['data'])
self._init_done = True
return
std_lib_cache = await self._storage.load_async(
domain=self.DOMAIN, name='spec_std_lib', type_=dict)
if (
isinstance(std_lib_cache, dict)
and 'data' in std_lib_cache
and 'ts' in std_lib_cache
and isinstance(std_lib_cache['ts'], int)
and int(time.time()) - std_lib_cache['ts'] <
SPEC_STD_LIB_EFFECTIVE_TIME
):
# Use the cache if the update time is less than 14 day
_LOGGER.debug(
'use local spec std cache, ts->%s', std_lib_cache['ts'])
self._std_lib.from_dict(std_lib_cache['data'])
self._init_done = True
return
# Update spec std lib
spec_lib_new = await self.__request_spec_std_lib_async()
if spec_lib_new:
self._std_lib.init(spec_lib_new)
if self._storage:
if not await self._storage.save_async(
domain=self.DOMAIN, name='spec_std_lib',
data={
'data': self._std_lib.dump(),
'ts': int(time.time())
}
):
_LOGGER.error('save spec std lib failed')
if await self._std_lib.refresh_async():
if not await self._storage.save_async(
domain=self.DOMAIN, name='spec_std_lib',
data={
'data': self._std_lib.dump(),
'ts': int(time.time())
}
):
_LOGGER.error('save spec std lib failed')
else:
if std_lib_cache:
self._std_lib.init(std_lib_cache['data'])
_LOGGER.error('get spec std lib failed, use local cache')
if isinstance(std_lib_cache, dict) and 'data' in std_lib_cache:
self._std_lib.from_dict(std_lib_cache['data'])
_LOGGER.info('get spec std lib failed, use local cache')
else:
_LOGGER.error('get spec std lib failed')
_LOGGER.error('load spec std lib failed')
self._init_done = True
async def deinit_async(self) -> None:
self._init_done = False
self._std_lib.deinit()
# self._std_lib.deinit()
await self._bool_trans.deinit_async()
await self._multi_lang.deinit_async()
await self._spec_filter.deinit_async()
@ -562,18 +763,15 @@ class MIoTSpecParser:
"""MUST await init first !!!"""
if not urn_list:
return False
spec_std_new: dict = await self.__request_spec_std_lib_async()
if spec_std_new:
self._std_lib.init(spec_std_new)
if self._storage:
if not await self._storage.save_async(
domain=self.DOMAIN, name='spec_std_lib',
data={
'data': self._std_lib.dump(),
'ts': int(time.time())
}
):
_LOGGER.error('save spec std lib failed')
if await self._std_lib.refresh_async():
if not await self._storage.save_async(
domain=self.DOMAIN, name='spec_std_lib',
data={
'data': self._std_lib.dump(),
'ts': int(time.time())
}
):
_LOGGER.error('save spec std lib failed')
else:
raise MIoTSpecError('get spec std lib failed')
success_count = 0
@ -585,28 +783,6 @@ class MIoTSpecParser:
success_count += sum(1 for result in results if result is not None)
return success_count
def __http_get(
self, url: str, params: dict = None, headers: dict = None
) -> dict:
if params:
encoded_params = urlencode(params)
full_url = f'{url}?{encoded_params}'
else:
full_url = url
request = Request(full_url, method='GET', headers=headers or {})
content: bytes = None
with urlopen(request) as response:
content = response.read()
return (
json.loads(str(content, 'utf-8'))
if content is not None else None)
async def __http_get_async(
self, url: str, params: dict = None, headers: dict = None
) -> dict:
return await self._main_loop.run_in_executor(
None, self.__http_get, url, params, headers)
async def __cache_get(self, urn: str) -> Optional[dict]:
if self._storage is not None:
if platform.system() == 'Windows':
@ -630,157 +806,20 @@ class MIoTSpecParser:
return {'string': 'str', 'bool': 'bool', 'float': 'float'}.get(
format_, 'int')
async def __request_spec_std_lib_async(self) -> Optional[SpecStdLib]:
std_libs: dict = None
for index in range(3):
try:
tasks: list = []
# Get std lib
for name in [
'device', 'service', 'property', 'event', 'action']:
tasks.append(self.__get_template_list(
'https://miot-spec.org/miot-spec-v2/template/list/'
+ name))
tasks.append(self.__get_property_value())
# Async request
results = await asyncio.gather(*tasks)
if None in results:
raise MIoTSpecError('init failed, None in result')
std_libs = {
'devices': results[0],
'services': results[1],
'properties': results[2],
'events': results[3],
'actions': results[4],
'values': results[5],
}
# Get external std lib, Power by LM
tasks.clear()
for name in [
'device', 'service', 'property', 'event', 'action',
'property_value']:
tasks.append(self.__http_get_async(
'https://cdn.cnbj1.fds.api.mi-img.com/res-conf/'
f'xiaomi-home/std_ex_{name}.json'))
results = await asyncio.gather(*tasks)
if results[0]:
for key, value in results[0].items():
if key in std_libs['devices']:
std_libs['devices'][key].update(value)
else:
std_libs['devices'][key] = value
else:
_LOGGER.error('get external std lib failed, devices')
if results[1]:
for key, value in results[1].items():
if key in std_libs['services']:
std_libs['services'][key].update(value)
else:
std_libs['services'][key] = value
else:
_LOGGER.error('get external std lib failed, services')
if results[2]:
for key, value in results[2].items():
if key in std_libs['properties']:
std_libs['properties'][key].update(value)
else:
std_libs['properties'][key] = value
else:
_LOGGER.error('get external std lib failed, properties')
if results[3]:
for key, value in results[3].items():
if key in std_libs['events']:
std_libs['events'][key].update(value)
else:
std_libs['events'][key] = value
else:
_LOGGER.error('get external std lib failed, events')
if results[4]:
for key, value in results[4].items():
if key in std_libs['actions']:
std_libs['actions'][key].update(value)
else:
std_libs['actions'][key] = value
else:
_LOGGER.error('get external std lib failed, actions')
if results[5]:
for key, value in results[5].items():
if key in std_libs['values']:
std_libs['values'][key].update(value)
else:
std_libs['values'][key] = value
else:
_LOGGER.error(
'get external std lib failed, values')
return std_libs
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error(
'update spec std lib error, retry, %d, %s', index, err)
return None
async def __get_property_value(self) -> dict:
reply = await self.__http_get_async(
url='https://miot-spec.org/miot-spec-v2'
'/normalization/list/property_value')
if reply is None or 'result' not in reply:
raise MIoTSpecError('get property value failed')
result = {}
for item in reply['result']:
if (
not isinstance(item, dict)
or 'normalization' not in item
or 'description' not in item
or 'proName' not in item
or 'urn' not in item
):
continue
result[
f'{item["urn"]}|{item["proName"]}|{item["normalization"]}'
] = {
'zh-Hans': item['description'],
'en': item['normalization']
}
return result
async def __get_template_list(self, url: str) -> dict:
reply = await self.__http_get_async(url=url)
if reply is None or 'result' not in reply:
raise MIoTSpecError(f'get service failed, {url}')
result: dict = {}
for item in reply['result']:
if (
not isinstance(item, dict)
or 'type' not in item
or 'description' not in item
):
continue
if 'zh_cn' in item['description']:
item['description']['zh-Hans'] = item['description'].pop(
'zh_cn')
if 'zh_hk' in item['description']:
item['description']['zh-Hant'] = item['description'].pop(
'zh_hk')
item['description'].pop('zh_tw', None)
elif 'zh_tw' in item['description']:
item['description']['zh-Hant'] = item['description'].pop(
'zh_tw')
result[item['type']] = item['description']
return result
async def __get_instance(self, urn: str) -> dict:
return await self.__http_get_async(
async def __get_instance(self, urn: str) -> Optional[dict]:
return await MIoTHttp.get_json_async(
url='https://miot-spec.org/miot-spec-v2/instance',
params={'type': urn})
async def __get_translation(self, urn: str) -> dict:
return await self.__http_get_async(
async def __get_translation(self, urn: str) -> Optional[dict]:
return await MIoTHttp.get_json_async(
url='https://miot-spec.org/instance/v2/multiLanguage',
params={'urn': urn})
async def __parse(self, urn: str) -> MIoTSpecInstance:
_LOGGER.debug('parse urn, %s', urn)
# Load spec instance
instance: dict = await self.__get_instance(urn=urn)
instance = await self.__get_instance(urn=urn)
if (
not isinstance(instance, dict)
or 'type' not in instance
@ -789,6 +828,8 @@ class MIoTSpecParser:
):
raise MIoTSpecError(f'invalid urn instance, {urn}')
translation: dict = {}
urn_strs: list[str] = urn.split(':')
urn_key: str = ':'.join(urn_strs[:6])
try:
# Load multiple language configuration.
res_trans = await self.__get_translation(urn=urn)
@ -798,9 +839,7 @@ class MIoTSpecParser:
or not isinstance(res_trans['data'], dict)
):
raise MIoTSpecError('invalid translation data')
urn_strs: list[str] = urn.split(':')
urn_key: str = ':'.join(urn_strs[:6])
trans_data: dict[str, str] = None
trans_data: dict[str, str] = {}
if self._lang == 'zh-Hans':
# Simplified Chinese
trans_data = res_trans['data'].get('zh_cn', {})

View File

@ -59,5 +59,10 @@
"1",
"5"
]
},
"urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03": {
"services": [
"*"
]
}
}

View File

@ -289,7 +289,7 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
}
},
'optional': {
'properties': {'mode', 'horizontal-swing'}
'properties': {'mode', 'horizontal-swing', 'wind-reverse'}
},
'entity': 'fan'
},