diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4f36d..22fdb9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## v0.1.5b0 +### Added +- Add missing parameter state_class [#101](https://github.com/XiaoMi/ha_xiaomi_home/pull/101) +### Changed +- Make git update guide more accurate [#561](https://github.com/XiaoMi/ha_xiaomi_home/pull/561) +### Fixed +- Limit *light.mode count (value-range) [#535](https://github.com/XiaoMi/ha_xiaomi_home/pull/535) +- Update miot cloud raise error msg [#551](https://github.com/XiaoMi/ha_xiaomi_home/pull/551) +- Fix table header misplacement [#554](https://github.com/XiaoMi/ha_xiaomi_home/pull/554) + ## v0.1.4 ### Added - Refactor miot network, add network detection logic, improve devices filter logic. [458](https://github.com/XiaoMi/ha_xiaomi_home/pull/458) [#191](https://github.com/XiaoMi/ha_xiaomi_home/pull/191) diff --git a/README.md b/README.md index 642c499..e6539e7 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ For example, update to version v1.0.0 ```bash cd config/ha_xiaomi_home +git fetch git checkout v1.0.0 ./install.sh /config ``` @@ -350,7 +351,7 @@ The instance code is the code of the MIoT-Spec-V2 instance, which is in the form ``` service: # service service::property: # property -service::property::valuelist: # the value in value-list of a property +service::property::valuelist: # The index of a value in the value-list of a property service::event: # event service::action: # action ``` diff --git a/custom_components/xiaomi_home/fan.py b/custom_components/xiaomi_home/fan.py index 42947ce..90220db 100644 --- a/custom_components/xiaomi_home/fan.py +++ b/custom_components/xiaomi_home/fan.py @@ -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 diff --git a/custom_components/xiaomi_home/light.py b/custom_components/xiaomi_home/light.py index bcfef6c..666464e 100644 --- a/custom_components/xiaomi_home/light.py +++ b/custom_components/xiaomi_home/light.py @@ -95,6 +95,7 @@ async def async_setup_entry( class Light(MIoTServiceEntity, LightEntity): """Light entities for Xiaomi Home.""" # pylint: disable=unused-argument + _VALUE_RANGE_MODE_COUNT_MAX = 30 _prop_on: Optional[MIoTSpecProperty] _prop_brightness: Optional[MIoTSpecProperty] _prop_color_temp: Optional[MIoTSpecProperty] @@ -147,13 +148,13 @@ class Light(MIoTServiceEntity, LightEntity): self._attr_supported_features |= LightEntityFeature.EFFECT self._prop_mode = prop else: - _LOGGER.error( + _LOGGER.info( 'invalid brightness format, %s', self.entity_id) continue # color-temperature if prop.name == 'color-temperature': if not isinstance(prop.value_range, dict): - _LOGGER.error( + _LOGGER.info( 'invalid color-temperature value_range format, %s', self.entity_id) continue @@ -179,16 +180,29 @@ class Light(MIoTServiceEntity, LightEntity): for item in prop.value_list} elif isinstance(prop.value_range, dict): mode_list = {} - for value in range( - prop.value_range['min'], prop.value_range['max']): - mode_list[value] = f'{value}' + if ( + int(( + prop.value_range['max'] + - prop.value_range['min'] + ) / prop.value_range['step']) + > self._VALUE_RANGE_MODE_COUNT_MAX + ): + _LOGGER.info( + 'too many mode values, %s, %s, %s', + self.entity_id, prop.name, prop.value_range) + else: + for value in range( + prop.value_range['min'], + prop.value_range['max'], + prop.value_range['step']): + mode_list[value] = f'mode {value}' if mode_list: self._mode_list = mode_list self._attr_effect_list = list(self._mode_list.values()) self._attr_supported_features |= LightEntityFeature.EFFECT self._prop_mode = prop else: - _LOGGER.error('invalid mode format, %s', self.entity_id) + _LOGGER.info('invalid mode format, %s', self.entity_id) continue if not self._attr_supported_color_modes: diff --git a/custom_components/xiaomi_home/manifest.json b/custom_components/xiaomi_home/manifest.json index 79a6bdb..3e07f1c 100644 --- a/custom_components/xiaomi_home/manifest.json +++ b/custom_components/xiaomi_home/manifest.json @@ -25,7 +25,7 @@ "cryptography", "psutil" ], - "version": "v0.1.4", + "version": "v0.1.5b0", "zeroconf": [ "_miot-central._tcp.local." ] diff --git a/custom_components/xiaomi_home/miot/lan/profile_models.yaml b/custom_components/xiaomi_home/miot/lan/profile_models.yaml index 5e14086..e56cc06 100644 --- a/custom_components/xiaomi_home/miot/lan/profile_models.yaml +++ b/custom_components/xiaomi_home/miot/lan/profile_models.yaml @@ -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 diff --git a/custom_components/xiaomi_home/miot/miot_cloud.py b/custom_components/xiaomi_home/miot/miot_cloud.py index 751defd..7ed3875 100644 --- a/custom_components/xiaomi_home/miot/miot_cloud.py +++ b/custom_components/xiaomi_home/miot/miot_cloud.py @@ -166,7 +166,7 @@ class MIoTOauthClient: key in res_obj['result'] for key in ['access_token', 'refresh_token', 'expires_in']) ): - raise MIoTOauthError(f'invalid http response, {http_res.text}') + raise MIoTOauthError(f'invalid http response, {res_str}') return { **res_obj['result'], @@ -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 diff --git a/custom_components/xiaomi_home/miot/miot_device.py b/custom_components/xiaomi_home/miot/miot_device.py index 47e2bc1..353b28f 100644 --- a/custom_components/xiaomi_home/miot/miot_device.py +++ b/custom_components/xiaomi_home/miot/miot_device.py @@ -515,9 +515,15 @@ class MIoTDevice: prop.icon = self.icon_convert(prop.unit) device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][ 'device_class'] - prop.platform = device_class - - return {'platform': platform, 'device_class': device_class} + 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.""" @@ -544,6 +550,13 @@ class MIoTDevice: 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: diff --git a/custom_components/xiaomi_home/miot/miot_spec.py b/custom_components/xiaomi_home/miot/miot_spec.py index bae7e79..33022a1 100644 --- a/custom_components/xiaomi_home/miot/miot_spec.py +++ b/custom_components/xiaomi_home/miot/miot_spec.py @@ -79,6 +79,7 @@ class MIoTSpecBase: # External params platform: str device_class: Any + state_class: Any icon: str external_unit: Any @@ -96,6 +97,7 @@ class MIoTSpecBase: self.platform = None self.device_class = None + self.state_class = None self.icon = None self.external_unit = None diff --git a/custom_components/xiaomi_home/miot/specs/spec_filter.json b/custom_components/xiaomi_home/miot/specs/spec_filter.json index 5cea69f..274fb34 100644 --- a/custom_components/xiaomi_home/miot/specs/spec_filter.json +++ b/custom_components/xiaomi_home/miot/specs/spec_filter.json @@ -59,5 +59,10 @@ "1", "5" ] + }, + "urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03": { + "services": [ + "*" + ] } } \ No newline at end of file diff --git a/custom_components/xiaomi_home/miot/specs/specv2entity.py b/custom_components/xiaomi_home/miot/specs/specv2entity.py index 4a57867..9e36011 100644 --- a/custom_components/xiaomi_home/miot/specs/specv2entity.py +++ b/custom_components/xiaomi_home/miot/specs/specv2entity.py @@ -46,8 +46,16 @@ off Xiaomi or its affiliates' products. Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity. """ from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor import SensorStateClass from homeassistant.components.event import EventDeviceClass +from homeassistant.const import ( + UnitOfEnergy, + UnitOfPower, + UnitOfElectricCurrent, + UnitOfElectricPotential, +) + # pylint: disable=pointless-string-statement """SPEC_DEVICE_TRANS_MAP { @@ -281,7 +289,7 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = { } }, 'optional': { - 'properties': {'mode', 'horizontal-swing'} + 'properties': {'mode', 'horizontal-swing', 'wind-reverse'} }, 'entity': 'fan' }, @@ -325,7 +333,11 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = { 'properties': { '':{ 'device_class': str, - 'entity': str + 'entity': str, + 'optional':{ + 'state_class': str, + 'unit_of_measurement': str + } } } } @@ -381,7 +393,11 @@ SPEC_PROP_TRANS_MAP: dict[str, dict | str] = { }, 'voltage': { 'device_class': SensorDeviceClass.VOLTAGE, - 'entity': 'sensor' + 'entity': 'sensor', + 'optional': { + 'state_class': SensorStateClass.MEASUREMENT, + 'unit_of_measurement': UnitOfElectricPotential.VOLT + } }, 'illumination': { 'device_class': SensorDeviceClass.ILLUMINANCE, @@ -391,6 +407,38 @@ SPEC_PROP_TRANS_MAP: dict[str, dict | str] = { 'device_class': SensorDeviceClass.DURATION, 'entity': 'sensor' }, + 'electric-power': { + 'device_class': SensorDeviceClass.POWER, + 'entity': 'sensor', + 'optional': { + 'state_class': SensorStateClass.MEASUREMENT, + 'unit_of_measurement': UnitOfPower.WATT + } + }, + 'electric-current': { + 'device_class': SensorDeviceClass.CURRENT, + 'entity': 'sensor', + 'optional': { + 'state_class': SensorStateClass.MEASUREMENT, + 'unit_of_measurement': UnitOfElectricCurrent.AMPERE + } + }, + 'power-consumption': { + 'device_class': SensorDeviceClass.ENERGY, + 'entity': 'sensor', + 'optional': { + 'state_class': SensorStateClass.TOTAL_INCREASING, + 'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR + } + }, + 'total-battery': { + 'device_class': SensorDeviceClass.ENERGY, + 'entity': 'sensor', + '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' } diff --git a/custom_components/xiaomi_home/sensor.py b/custom_components/xiaomi_home/sensor.py index b5ff00d..39b3bdb 100644 --- a/custom_components/xiaomi_home/sensor.py +++ b/custom_components/xiaomi_home/sensor.py @@ -106,6 +106,9 @@ class Sensor(MIoTPropertyEntity, SensorEntity): # Set icon if spec.icon: self._attr_icon = spec.icon + # Set state_class + if spec.state_class: + self._attr_state_class = spec.state_class @property def native_value(self) -> Any: diff --git a/doc/README_zh.md b/doc/README_zh.md index b237b8b..438771f 100644 --- a/doc/README_zh.md +++ b/doc/README_zh.md @@ -26,6 +26,7 @@ cd ha_xiaomi_home ```bash cd config/ha_xiaomi_home +git fetch git checkout v1.0.0 ./install.sh /config ```