Compare commits

...

14 Commits

Author SHA1 Message Date
Feng Wang
6d1733c00b
Merge 0668985969 into 5d4b975f85 2025-01-07 20:22:55 +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
Feng Wang
0668985969 fix access violation 2024-12-31 19:36:15 +08:00
Feng Wang
48a931b9f6 remove tev 2024-12-31 18:48:51 +08:00
Feng Wang
ab8a15f40d Merge branch 'main' into remove-tev-from-mips 2024-12-31 18:47:45 +08:00
Feng Wang
55d27409b7 fix the deleted public data class 2024-12-23 17:03:09 +08:00
Feng Wang
d1c49bdf7b fix thread naming 2024-12-23 16:59:45 +08:00
Feng Wang
23bf6817b9 simplify inheritance 2024-12-23 16:43:34 +08:00
Feng Wang
43fd2bf42e make private classes private 2024-12-23 16:26:25 +08:00
Feng Wang
8fcb4d895c lint fix 2024-12-23 16:22:44 +08:00
Feng Wang
8778321b50 remove use of tev & fix type errors 2024-12-22 23:49:25 +08:00
10 changed files with 896 additions and 1071 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

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

@ -1,324 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2024 Xiaomi Corporation.
The ownership and intellectual property rights of Xiaomi Home Assistant
Integration and related Xiaomi cloud service API interface provided under this
license, including source code and object code (collectively, "Licensed Work"),
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
hereby grants you a personal, limited, non-exclusive, non-transferable,
non-sublicensable, and royalty-free license to reproduce, use, modify, and
distribute the Licensed Work only for your use of Home Assistant for
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
you to use the Licensed Work for any other purpose, including but not limited
to use Licensed Work to develop applications (APP), Web services, and other
forms of software.
You may reproduce and distribute copies of the Licensed Work, with or without
modifications, whether in source or object form, provided that you must give
any other recipients of the Licensed Work a copy of this License and retain all
copyright and disclaimers.
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied, including, without
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
for any direct, indirect, special, incidental, or consequential damages or
losses arising from the use or inability to use the Licensed Work.
Xiaomi reserves all rights not expressly granted to you in this License.
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
does not authorize you in any form to use the trademarks, copyrights, or other
forms of intellectual property rights of Xiaomi and its affiliates, including,
without limitation, without obtaining other written permission from Xiaomi, you
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
may make the public associate with Xiaomi in any form to publicize or promote
the software or hardware devices that use the Licensed Work.
Xiaomi has the right to immediately terminate all your authorization under this
License in the event:
1. You assert patent invalidation, litigation, or other claims against patents
or other intellectual property rights of Xiaomi or its affiliates; or,
2. You make, have made, manufacture, sell, or offer to sell products that knock
off Xiaomi or its affiliates' products.
MIoT event loop.
"""
import selectors
import heapq
import time
import traceback
from typing import Any, Callable, TypeVar
import logging
import threading
# pylint: disable=relative-beyond-top-level
from .miot_error import MIoTEvError
_LOGGER = logging.getLogger(__name__)
TimeoutHandle = TypeVar('TimeoutHandle')
class MIoTFdHandler:
"""File descriptor handler."""
fd: int
read_handler: Callable[[Any], None]
read_handler_ctx: Any
write_handler: Callable[[Any], None]
write_handler_ctx: Any
def __init__(
self, fd: int,
read_handler: Callable[[Any], None] = None,
read_handler_ctx: Any = None,
write_handler: Callable[[Any], None] = None,
write_handler_ctx: Any = None
) -> None:
self.fd = fd
self.read_handler = read_handler
self.read_handler_ctx = read_handler_ctx
self.write_handler = write_handler
self.write_handler_ctx = write_handler_ctx
class MIoTTimeout:
"""Timeout handler."""
key: TimeoutHandle
target: int
handler: Callable[[Any], None]
handler_ctx: Any
def __init__(
self, key: str = None, target: int = None,
handler: Callable[[Any], None] = None,
handler_ctx: Any = None
) -> None:
self.key = key
self.target = target
self.handler = handler
self.handler_ctx = handler_ctx
def __lt__(self, other):
return self.target < other.target
class MIoTEventLoop:
"""MIoT event loop."""
_poll_fd: selectors.DefaultSelector
_fd_handlers: dict[str, MIoTFdHandler]
_timer_heap: list[MIoTTimeout]
_timer_handlers: dict[str, MIoTTimeout]
_timer_handle_seed: int
# Label if the current fd handler is freed inside a read handler to
# avoid invalid reading.
_fd_handler_freed_in_read_handler: bool
def __init__(self) -> None:
self._poll_fd = selectors.DefaultSelector()
self._timer_heap = []
self._timer_handlers = {}
self._timer_handle_seed = 1
self._fd_handlers = {}
self._fd_handler_freed_in_read_handler = False
def loop_forever(self) -> None:
"""Run an event loop in current thread."""
next_timeout: int
while True:
next_timeout = 0
# Handle timer
now_ms: int = self.__get_monotonic_ms
while len(self._timer_heap) > 0:
timer: MIoTTimeout = self._timer_heap[0]
if timer is None:
break
if timer.target <= now_ms:
heapq.heappop(self._timer_heap)
del self._timer_handlers[timer.key]
if timer.handler:
timer.handler(timer.handler_ctx)
else:
next_timeout = timer.target-now_ms
break
# Are there any files to listen to
if next_timeout == 0 and self._fd_handlers:
next_timeout = None # None == infinite
# Wait for timers & fds
if next_timeout == 0:
# Neither timer nor fds exist, exit loop
break
# Handle fd event
events = self._poll_fd.select(
timeout=next_timeout/1000.0 if next_timeout else next_timeout)
for key, mask in events:
fd_handler: MIoTFdHandler = key.data
if fd_handler is None:
continue
self._fd_handler_freed_in_read_handler = False
fd_key = str(id(fd_handler.fd))
if fd_key not in self._fd_handlers:
continue
if (
mask & selectors.EVENT_READ > 0
and fd_handler.read_handler
):
fd_handler.read_handler(fd_handler.read_handler_ctx)
if (
mask & selectors.EVENT_WRITE > 0
and self._fd_handler_freed_in_read_handler is False
and fd_handler.write_handler
):
fd_handler.write_handler(fd_handler.write_handler_ctx)
def loop_stop(self) -> None:
"""Stop the event loop."""
if self._poll_fd:
self._poll_fd.close()
self._poll_fd = None
self._fd_handlers = {}
self._timer_heap = []
self._timer_handlers = {}
def set_timeout(
self, timeout_ms: int, handler: Callable[[Any], None],
handler_ctx: Any = None
) -> TimeoutHandle:
"""Set a timer."""
if timeout_ms is None or handler is None:
raise MIoTEvError('invalid params')
new_timeout: MIoTTimeout = MIoTTimeout()
new_timeout.key = self.__get_next_timeout_handle
new_timeout.target = self.__get_monotonic_ms + timeout_ms
new_timeout.handler = handler
new_timeout.handler_ctx = handler_ctx
heapq.heappush(self._timer_heap, new_timeout)
self._timer_handlers[new_timeout.key] = new_timeout
return new_timeout.key
def clear_timeout(self, timer_key: TimeoutHandle) -> None:
"""Stop and remove the timer."""
if timer_key is None:
return
timer: MIoTTimeout = self._timer_handlers.pop(timer_key, None)
if timer:
self._timer_heap = list(self._timer_heap)
self._timer_heap.remove(timer)
heapq.heapify(self._timer_heap)
def set_read_handler(
self, fd: int, handler: Callable[[Any], None], handler_ctx: Any = None
) -> bool:
"""Set a read handler for a file descriptor.
Returns:
bool: True, success. False, failed.
"""
self.__set_handler(
fd, is_read=True, handler=handler, handler_ctx=handler_ctx)
def set_write_handler(
self, fd: int, handler: Callable[[Any], None], handler_ctx: Any = None
) -> bool:
"""Set a write handler for a file descriptor.
Returns:
bool: True, success. False, failed.
"""
self.__set_handler(
fd, is_read=False, handler=handler, handler_ctx=handler_ctx)
def __set_handler(
self, fd, is_read: bool, handler: Callable[[Any], None],
handler_ctx: Any = None
) -> bool:
"""Set a handler."""
if fd is None:
raise MIoTEvError('invalid params')
if not self._poll_fd:
raise MIoTEvError('event loop not started')
fd_key: str = str(id(fd))
fd_handler = self._fd_handlers.get(fd_key, None)
if fd_handler is None:
fd_handler = MIoTFdHandler(fd=fd)
fd_handler.fd = fd
self._fd_handlers[fd_key] = fd_handler
read_handler_existed = fd_handler.read_handler is not None
write_handler_existed = fd_handler.write_handler is not None
if is_read is True:
fd_handler.read_handler = handler
fd_handler.read_handler_ctx = handler_ctx
else:
fd_handler.write_handler = handler
fd_handler.write_handler_ctx = handler_ctx
if fd_handler.read_handler is None and fd_handler.write_handler is None:
# Remove from epoll and map
try:
self._poll_fd.unregister(fd)
except (KeyError, ValueError, OSError) as e:
del e
self._fd_handlers.pop(fd_key, None)
# May be inside a read handler, if not, this has no effect
self._fd_handler_freed_in_read_handler = True
elif read_handler_existed is False and write_handler_existed is False:
# Add to epoll
events = 0x0
if fd_handler.read_handler:
events |= selectors.EVENT_READ
if fd_handler.write_handler:
events |= selectors.EVENT_WRITE
try:
self._poll_fd.register(fd, events=events, data=fd_handler)
except (KeyError, ValueError, OSError) as e:
_LOGGER.error(
'%s, register fd, error, %s, %s, %s, %s, %s',
threading.current_thread().name,
'read' if is_read else 'write',
fd_key, handler, e, traceback.format_exc())
self._fd_handlers.pop(fd_key, None)
return False
elif (
read_handler_existed != (fd_handler.read_handler is not None)
or write_handler_existed != (fd_handler.write_handler is not None)
):
# Modify epoll
events = 0x0
if fd_handler.read_handler:
events |= selectors.EVENT_READ
if fd_handler.write_handler:
events |= selectors.EVENT_WRITE
try:
self._poll_fd.modify(fd, events=events, data=fd_handler)
except (KeyError, ValueError, OSError) as e:
_LOGGER.error(
'%s, modify fd, error, %s, %s, %s, %s, %s',
threading.current_thread().name,
'read' if is_read else 'write',
fd_key, handler, e, traceback.format_exc())
self._fd_handlers.pop(fd_key, None)
return False
return True
@property
def __get_next_timeout_handle(self) -> str:
# Get next timeout handle, that is not larger than the maximum
# value of UINT64 type.
self._timer_handle_seed += 1
# uint64 max
self._timer_handle_seed %= 0xFFFFFFFFFFFFFFFF
return str(self._timer_handle_seed)
@property
def __get_monotonic_ms(self) -> int:
"""Get monotonic ms timestamp."""
return int(time.monotonic()*1000)

File diff suppressed because it is too large Load Diff

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'
},

View File

@ -20,7 +20,6 @@ def load_py_file():
'const.py',
'miot_cloud.py',
'miot_error.py',
'miot_ev.py',
'miot_i18n.py',
'miot_lan.py',
'miot_mdns.py',

View File

@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
"""Unit test for miot_ev.py."""
import os
import pytest
# pylint: disable=import-outside-toplevel, disable=unused-argument
@pytest.mark.github
def test_mev_timer_and_fd():
from miot.miot_ev import MIoTEventLoop, TimeoutHandle
mev = MIoTEventLoop()
assert mev
event_fd: os.eventfd = os.eventfd(0, os.O_NONBLOCK)
assert event_fd
timer4: TimeoutHandle = None
def event_handler(event_fd):
value: int = os.eventfd_read(event_fd)
if value == 1:
mev.clear_timeout(timer4)
print('cancel timer4')
elif value == 2:
print('event write twice in a row')
elif value == 3:
mev.set_read_handler(event_fd, None, None)
os.close(event_fd)
event_fd = None
print('close event fd')
def timer1_handler(event_fd):
os.eventfd_write(event_fd, 1)
def timer2_handler(event_fd):
os.eventfd_write(event_fd, 1)
os.eventfd_write(event_fd, 1)
def timer3_handler(event_fd):
os.eventfd_write(event_fd, 3)
def timer4_handler(event_fd):
raise ValueError('unreachable code')
mev.set_read_handler(
event_fd, event_handler, event_fd)
mev.set_timeout(500, timer1_handler, event_fd)
mev.set_timeout(1000, timer2_handler, event_fd)
mev.set_timeout(1500, timer3_handler, event_fd)
timer4 = mev.set_timeout(2000, timer4_handler, event_fd)
mev.loop_forever()
# Loop will exit when there are no timers or fd handlers.
mev.loop_stop()