diff --git a/miio/airconditioner_miot.py b/miio/airconditioner_miot.py index 0fe355546..3a9de07c5 100644 --- a/miio/airconditioner_miot.py +++ b/miio/airconditioner_miot.py @@ -47,6 +47,9 @@ "timer": {"siid": 10, "piid": 3}, } +_MAPPINGS = {model: _MAPPING for model in SUPPORTED_MODELS} + + CLEANING_STAGES = [ "Stopped", "Condensing water", @@ -281,8 +284,7 @@ def timer(self) -> TimerStatus: class AirConditionerMiot(MiotDevice): """Main class representing the air conditioner which uses MIoT protocol.""" - _supported_models = SUPPORTED_MODELS - mapping = _MAPPING + _mappings = _MAPPINGS @command( default_output=format_output( diff --git a/miio/airhumidifier_miot.py b/miio/airhumidifier_miot.py index 21e629c9d..577c0db1d 100644 --- a/miio/airhumidifier_miot.py +++ b/miio/airhumidifier_miot.py @@ -9,32 +9,39 @@ from .miot_device import DeviceStatus, MiotDevice _LOGGER = logging.getLogger(__name__) -_MAPPING = { - # Source http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:humidifier:0000A00E:zhimi-ca4:2 - # Air Humidifier (siid=2) - "power": {"siid": 2, "piid": 1}, # bool - "fault": {"siid": 2, "piid": 2}, # [0, 15] step 1 - "mode": {"siid": 2, "piid": 5}, # 0 - Auto, 1 - lvl1, 2 - lvl2, 3 - lvl3 - "target_humidity": {"siid": 2, "piid": 6}, # [30, 80] step 1 - "water_level": {"siid": 2, "piid": 7}, # [0, 128] step 1 - "dry": {"siid": 2, "piid": 8}, # bool - "use_time": {"siid": 2, "piid": 9}, # [0, 2147483600], step 1 - "button_pressed": {"siid": 2, "piid": 10}, # 0 - none, 1 - led, 2 - power - "speed_level": {"siid": 2, "piid": 11}, # [200, 2000], step 10 - # Environment (siid=3) - "temperature": {"siid": 3, "piid": 7}, # [-40, 125] step 0.1 - "fahrenheit": {"siid": 3, "piid": 8}, # [-40, 257] step 0.1 - "humidity": {"siid": 3, "piid": 9}, # [0, 100] step 1 - # Alarm (siid=4) - "buzzer": {"siid": 4, "piid": 1}, - # Indicator Light (siid=5) - "led_brightness": {"siid": 5, "piid": 2}, # 0 - Off, 1 - Dim, 2 - Brightest - # Physical Control Locked (siid=6) - "child_lock": {"siid": 6, "piid": 1}, # bool - # Other (siid=7) - "actual_speed": {"siid": 7, "piid": 1}, # [0, 2000] step 1 - "power_time": {"siid": 7, "piid": 3}, # [0, 4294967295] step 1 - "clean_mode": {"siid": 7, "piid": 5}, # bool + + +SMARTMI_EVAPORATIVE_HUMIDIFIER_2 = "zhimi.humidifier.ca4" + + +_MAPPINGS = { + SMARTMI_EVAPORATIVE_HUMIDIFIER_2: { + # Source http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:humidifier:0000A00E:zhimi-ca4:2 + # Air Humidifier (siid=2) + "power": {"siid": 2, "piid": 1}, # bool + "fault": {"siid": 2, "piid": 2}, # [0, 15] step 1 + "mode": {"siid": 2, "piid": 5}, # 0 - Auto, 1 - lvl1, 2 - lvl2, 3 - lvl3 + "target_humidity": {"siid": 2, "piid": 6}, # [30, 80] step 1 + "water_level": {"siid": 2, "piid": 7}, # [0, 128] step 1 + "dry": {"siid": 2, "piid": 8}, # bool + "use_time": {"siid": 2, "piid": 9}, # [0, 2147483600], step 1 + "button_pressed": {"siid": 2, "piid": 10}, # 0 - none, 1 - led, 2 - power + "speed_level": {"siid": 2, "piid": 11}, # [200, 2000], step 10 + # Environment (siid=3) + "temperature": {"siid": 3, "piid": 7}, # [-40, 125] step 0.1 + "fahrenheit": {"siid": 3, "piid": 8}, # [-40, 257] step 0.1 + "humidity": {"siid": 3, "piid": 9}, # [0, 100] step 1 + # Alarm (siid=4) + "buzzer": {"siid": 4, "piid": 1}, + # Indicator Light (siid=5) + "led_brightness": {"siid": 5, "piid": 2}, # 0 - Off, 1 - Dim, 2 - Brightest + # Physical Control Locked (siid=6) + "child_lock": {"siid": 6, "piid": 1}, # bool + # Other (siid=7) + "actual_speed": {"siid": 7, "piid": 1}, # [0, 2000] step 1 + "power_time": {"siid": 7, "piid": 3}, # [0, 4294967295] step 1 + "clean_mode": {"siid": 7, "piid": 5}, # bool + } } @@ -248,17 +255,10 @@ def clean_mode(self) -> bool: return self.data["clean_mode"] -SMARTMI_EVAPORATIVE_HUMIDIFIER_2 = "zhimi.humidifier.ca4" - -SUPPORTED_MODELS = [SMARTMI_EVAPORATIVE_HUMIDIFIER_2] - - class AirHumidifierMiot(MiotDevice): """Main class representing the air humidifier which uses MIoT protocol.""" - _supported_models = SUPPORTED_MODELS - - mapping = _MAPPING + _mappings = _MAPPINGS @command( default_output=format_output( diff --git a/miio/airpurifier_miot.py b/miio/airpurifier_miot.py index 3b69db851..ca8eace89 100644 --- a/miio/airpurifier_miot.py +++ b/miio/airpurifier_miot.py @@ -320,7 +320,6 @@ def filter_left_time(self) -> Optional[int]: class AirPurifierMiot(MiotDevice): """Main class representing the air purifier which uses MIoT protocol.""" - _supported_models = list(_MAPPINGS.keys()) _mappings = _MAPPINGS @command( diff --git a/miio/airqualitymonitor_miot.py b/miio/airqualitymonitor_miot.py index 71c28a152..05bd39c9e 100644 --- a/miio/airqualitymonitor_miot.py +++ b/miio/airqualitymonitor_miot.py @@ -11,37 +11,39 @@ MODEL_AIRQUALITYMONITOR_CGDN1 = "cgllc.airm.cgdn1" -_MAPPING_CGDN1 = { - # Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-cgdn1:1 - # Environment - "humidity": {"siid": 3, "piid": 1}, # [0, 100] step 1 - "pm25": {"siid": 3, "piid": 4}, # [0, 1000] step 1 - "pm10": {"siid": 3, "piid": 5}, # [0, 1000] step 1 - "temperature": {"siid": 3, "piid": 7}, # [-30, 100] step 0.00001 - "co2": {"siid": 3, "piid": 8}, # [0, 9999] step 1 - # Battery - "battery": {"siid": 4, "piid": 1}, # [0, 100] step 1 - "charging_state": { - "siid": 4, - "piid": 2, - }, # 1 - Charging, 2 - Not charging, 3 - Not chargeable - "voltage": {"siid": 4, "piid": 3}, # [0, 65535] step 1 - # Settings - "start_time": {"siid": 9, "piid": 2}, # [0, 2147483647] step 1 - "end_time": {"siid": 9, "piid": 3}, # [0, 2147483647] step 1 - "monitoring_frequency": { - "siid": 9, - "piid": 4, - }, # 1, 60, 300, 600, 0; device accepts [0..600] - "screen_off": { - "siid": 9, - "piid": 5, - }, # 15, 30, 60, 300, 0; device accepts [0..300], 0 means never - "device_off": { - "siid": 9, - "piid": 6, - }, # 15, 30, 60, 0; device accepts [0..60], 0 means never - "temperature_unit": {"siid": 9, "piid": 7}, +_MAPPINGS = { + MODEL_AIRQUALITYMONITOR_CGDN1: { + # Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-cgdn1:1 + # Environment + "humidity": {"siid": 3, "piid": 1}, # [0, 100] step 1 + "pm25": {"siid": 3, "piid": 4}, # [0, 1000] step 1 + "pm10": {"siid": 3, "piid": 5}, # [0, 1000] step 1 + "temperature": {"siid": 3, "piid": 7}, # [-30, 100] step 0.00001 + "co2": {"siid": 3, "piid": 8}, # [0, 9999] step 1 + # Battery + "battery": {"siid": 4, "piid": 1}, # [0, 100] step 1 + "charging_state": { + "siid": 4, + "piid": 2, + }, # 1 - Charging, 2 - Not charging, 3 - Not chargeable + "voltage": {"siid": 4, "piid": 3}, # [0, 65535] step 1 + # Settings + "start_time": {"siid": 9, "piid": 2}, # [0, 2147483647] step 1 + "end_time": {"siid": 9, "piid": 3}, # [0, 2147483647] step 1 + "monitoring_frequency": { + "siid": 9, + "piid": 4, + }, # 1, 60, 300, 600, 0; device accepts [0..600] + "screen_off": { + "siid": 9, + "piid": 5, + }, # 15, 30, 60, 300, 0; device accepts [0..300], 0 means never + "device_off": { + "siid": 9, + "piid": 6, + }, # 15, 30, 60, 0; device accepts [0..60], 0 means never + "temperature_unit": {"siid": 9, "piid": 7}, + } } @@ -169,8 +171,7 @@ def display_temperature_unit(self): class AirQualityMonitorCGDN1(MiotDevice): """Qingping Air Monitor Lite.""" - mapping = _MAPPING_CGDN1 - _supported_models = [MODEL_AIRQUALITYMONITOR_CGDN1] + _mappings = _MAPPINGS @command( default_output=format_output( diff --git a/miio/click_common.py b/miio/click_common.py index dd1832bc8..c6ce5317d 100644 --- a/miio/click_common.py +++ b/miio/click_common.py @@ -145,6 +145,11 @@ def get_device_group(dcls): mcs._device_classes.add(cls) return cls + @property + def supported_models(cls): + """Return list of supported models.""" + return cls._mappings.keys() or cls._supported_models + class DeviceGroup(click.MultiCommand): class Command: diff --git a/miio/curtain_youpin.py b/miio/curtain_youpin.py index 538f41744..033169364 100644 --- a/miio/curtain_youpin.py +++ b/miio/curtain_youpin.py @@ -8,26 +8,33 @@ from .miot_device import DeviceStatus, MiotDevice _LOGGER = logging.getLogger(__name__) -_MAPPING = { - # # Source http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:curtain:0000A00C:lumi-hagl05:1 - # Curtain - "motor_control": {"siid": 2, "piid": 2}, # 0 - Pause, 1 - Open, 2 - Close, 3 - auto - "current_position": {"siid": 2, "piid": 3}, # Range: [0, 100, 1] - "status": {"siid": 2, "piid": 6}, # 0 - Stopped, 1 - Opening, 2 - Closing - "target_position": {"siid": 2, "piid": 7}, # Range: [0, 100, 1] - # curtain_cfg - "is_manual_enabled": {"siid": 4, "piid": 1}, # - "polarity": {"siid": 4, "piid": 2}, - "is_position_limited": {"siid": 4, "piid": 3}, - "night_tip_light": {"siid": 4, "piid": 4}, - "run_time": {"siid": 4, "piid": 5}, # Range: [0, 255, 1] - # motor_controller - "adjust_value": {"siid": 5, "piid": 1}, # Range: [-100, 100, 1] -} + # Model: ZNCLDJ21LM (also known as "Xiaomiyoupin Curtain Controller (Wi-Fi)" MODEL_CURTAIN_HAGL05 = "lumi.curtain.hagl05" +_MAPPINGS = { + MODEL_CURTAIN_HAGL05: { + # # Source http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:curtain:0000A00C:lumi-hagl05:1 + # Curtain + "motor_control": { + "siid": 2, + "piid": 2, + }, # 0 - Pause, 1 - Open, 2 - Close, 3 - auto + "current_position": {"siid": 2, "piid": 3}, # Range: [0, 100, 1] + "status": {"siid": 2, "piid": 6}, # 0 - Stopped, 1 - Opening, 2 - Closing + "target_position": {"siid": 2, "piid": 7}, # Range: [0, 100, 1] + # curtain_cfg + "is_manual_enabled": {"siid": 4, "piid": 1}, # + "polarity": {"siid": 4, "piid": 2}, + "is_position_limited": {"siid": 4, "piid": 3}, + "night_tip_light": {"siid": 4, "piid": 4}, + "run_time": {"siid": 4, "piid": 5}, # Range: [0, 255, 1] + # motor_controller + "adjust_value": {"siid": 5, "piid": 1}, # Range: [-100, 100, 1] + } +} + class MotorControl(enum.Enum): Pause = 0 @@ -114,8 +121,7 @@ def adjust_value(self) -> int: class CurtainMiot(MiotDevice): """Main class representing the lumi.curtain.hagl05 curtain.""" - mapping = _MAPPING - _supported_models = ["lumi.curtain.hagl05"] + _mappings = _MAPPINGS @command( default_output=format_output( diff --git a/miio/device.py b/miio/device.py index 5c8b46ebc..ef2334aa4 100644 --- a/miio/device.py +++ b/miio/device.py @@ -3,7 +3,7 @@ import warnings from enum import Enum from pprint import pformat as pf -from typing import Any, List, Optional # noqa: F401 +from typing import Any, Dict, List, Optional # noqa: F401 import click @@ -57,6 +57,7 @@ class Device(metaclass=DeviceGroupMeta): retry_count = 3 timeout = 5 + _mappings: Dict[str, Any] = {} _supported_models: List[str] = [] def __init__( diff --git a/miio/heater_miot.py b/miio/heater_miot.py index 4554452ea..eed9c3d78 100644 --- a/miio/heater_miot.py +++ b/miio/heater_miot.py @@ -143,7 +143,6 @@ class HeaterMiot(MiotDevice): (zhimi.heater.za2).""" _mappings = _MAPPINGS - _supported_models = list(_MAPPINGS.keys()) @command( default_output=format_output( diff --git a/miio/integrations/fan/dmaker/fan_miot.py b/miio/integrations/fan/dmaker/fan_miot.py index 0f80a9680..794d92c1d 100644 --- a/miio/integrations/fan/dmaker/fan_miot.py +++ b/miio/integrations/fan/dmaker/fan_miot.py @@ -14,17 +14,6 @@ MIOT_MAPPING = { - MODEL_FAN_1C: { - # https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:fan:0000A005:dmaker-1c:1 - "power": {"siid": 2, "piid": 1}, - "fan_level": {"siid": 2, "piid": 2}, - "child_lock": {"siid": 3, "piid": 1}, - "swing_mode": {"siid": 2, "piid": 3}, - "power_off_time": {"siid": 2, "piid": 10}, - "buzzer": {"siid": 2, "piid": 11}, - "light": {"siid": 2, "piid": 12}, - "mode": {"siid": 2, "piid": 7}, - }, MODEL_FAN_P9: { # Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:fan:0000A005:dmaker-p9:1 "power": {"siid": 2, "piid": 1}, @@ -70,6 +59,20 @@ }, } +FAN1C_MAPPINGS = { + MODEL_FAN_1C: { + # https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:fan:0000A005:dmaker-1c:1 + "power": {"siid": 2, "piid": 1}, + "fan_level": {"siid": 2, "piid": 2}, + "child_lock": {"siid": 3, "piid": 1}, + "swing_mode": {"siid": 2, "piid": 3}, + "power_off_time": {"siid": 2, "piid": 10}, + "buzzer": {"siid": 2, "piid": 11}, + "light": {"siid": 2, "piid": 12}, + "mode": {"siid": 2, "piid": 7}, + } +} + SUPPORTED_ANGLES = { MODEL_FAN_P9: [30, 60, 90, 120, 150], MODEL_FAN_P10: [30, 60, 90, 120, 140], @@ -231,8 +234,6 @@ def child_lock(self) -> bool: class FanMiot(MiotDevice): _mappings = MIOT_MAPPING - # TODO Fan1C should be merged to FanMiot - _supported_models = list(set(MIOT_MAPPING) - {MODEL_FAN_1C}) @command( default_output=format_output( @@ -373,8 +374,8 @@ def set_rotate(self, direction: MoveDirection): class Fan1C(MiotDevice): - mapping = MIOT_MAPPING[MODEL_FAN_1C] - _supported_models = [MODEL_FAN_1C] + # TODO Fan1C should be merged to FanMiot, or moved into its separate file + _mappings = FAN1C_MAPPINGS def __init__( self, diff --git a/miio/integrations/fan/zhimi/zhimi_miot.py b/miio/integrations/fan/zhimi/zhimi_miot.py index 88a193835..0c07240cc 100644 --- a/miio/integrations/fan/zhimi/zhimi_miot.py +++ b/miio/integrations/fan/zhimi/zhimi_miot.py @@ -170,7 +170,6 @@ def temperature(self) -> Any: class FanZA5(MiotDevice): _mappings = MIOT_MAPPING - _supported_models = list(MIOT_MAPPING.keys()) @command( default_output=format_output( diff --git a/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py index 652b1fd15..f8ab1177d 100644 --- a/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py +++ b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py @@ -29,6 +29,9 @@ "overwet_protect": {"siid": 7, "piid": 3}, # bool } +SUPPORTED_MODELS = ["deerma.humidifier.jsqs", "deerma.humidifier.jsq5"] +MIOT_MAPPING = {model: _MAPPING for model in SUPPORTED_MODELS} + class AirHumidifierJsqsException(DeviceException): pass @@ -145,9 +148,7 @@ def overwet_protect(self) -> Optional[bool]: class AirHumidifierJsqs(MiotDevice): """Main class representing the air humidifier which uses MIoT protocol.""" - _supported_models = ["deerma.humidifier.jsqs", "deerma.humidifier.jsq5"] - - mapping = _MAPPING + _mappings = MIOT_MAPPING @command( default_output=format_output( diff --git a/miio/integrations/petwaterdispenser/device.py b/miio/integrations/petwaterdispenser/device.py index a16d833f7..64d3cd5d7 100644 --- a/miio/integrations/petwaterdispenser/device.py +++ b/miio/integrations/petwaterdispenser/device.py @@ -37,13 +37,14 @@ "timezone": {"siid": 9, "piid": 1}, } +MIOT_MAPPING = {model: _MAPPING for model in SUPPORTED_MODELS} + class PetWaterDispenser(MiotDevice): """Main class representing the Pet Waterer / Pet Drinking Fountain / Smart Pet Water Dispenser.""" - mapping = _MAPPING - _supported_models = SUPPORTED_MODELS + _mappings = MIOT_MAPPING @command( default_output=format_output( diff --git a/miio/integrations/vacuum/dreame/dreamevacuum_miot.py b/miio/integrations/vacuum/dreame/dreamevacuum_miot.py index 97c306961..b36b3d9e0 100644 --- a/miio/integrations/vacuum/dreame/dreamevacuum_miot.py +++ b/miio/integrations/vacuum/dreame/dreamevacuum_miot.py @@ -387,12 +387,6 @@ def is_water_box_carriage_attached(self) -> Optional[bool]: class DreameVacuum(MiotDevice): - _supported_models = [ - DREAME_1C, - DREAME_D9, - DREAME_F9, - DREAME_Z10_PRO, - ] _mappings = MIOT_MAPPING @command( diff --git a/miio/integrations/vacuum/mijia/g1vacuum.py b/miio/integrations/vacuum/mijia/g1vacuum.py index f6c5afd65..344be83f6 100644 --- a/miio/integrations/vacuum/mijia/g1vacuum.py +++ b/miio/integrations/vacuum/mijia/g1vacuum.py @@ -13,39 +13,39 @@ SUPPORTED_MODELS = [MIJIA_VACUUM_V1, MIJIA_VACUUM_V2] -MIOT_MAPPING = { - MIJIA_VACUUM_V2: { - # https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:vacuum:0000A006:mijia-v1:1 - "battery": {"siid": 3, "piid": 1}, - "charge_state": {"siid": 3, "piid": 2}, - "error_code": {"siid": 2, "piid": 2}, - "state": {"siid": 2, "piid": 1}, - "fan_speed": {"siid": 2, "piid": 6}, - "operating_mode": {"siid": 2, "piid": 4}, - "mop_state": {"siid": 16, "piid": 1}, - "water_level": {"siid": 2, "piid": 5}, - "main_brush_life_level": {"siid": 14, "piid": 1}, - "main_brush_time_left": {"siid": 14, "piid": 2}, - "side_brush_life_level": {"siid": 15, "piid": 1}, - "side_brush_time_left": {"siid": 15, "piid": 2}, - "filter_life_level": {"siid": 11, "piid": 1}, - "filter_time_left": {"siid": 11, "piid": 2}, - "clean_area": {"siid": 9, "piid": 1}, - "clean_time": {"siid": 9, "piid": 2}, - # totals always return 0 - "total_clean_area": {"siid": 9, "piid": 3}, - "total_clean_time": {"siid": 9, "piid": 4}, - "total_clean_count": {"siid": 9, "piid": 5}, - "home": {"siid": 2, "aiid": 3}, - "find": {"siid": 6, "aiid": 1}, - "start": {"siid": 2, "aiid": 1}, - "stop": {"siid": 2, "aiid": 2}, - "reset_main_brush_life_level": {"siid": 14, "aiid": 1}, - "reset_side_brush_life_level": {"siid": 15, "aiid": 1}, - "reset_filter_life_level": {"siid": 11, "aiid": 1}, - } +MAPPING = { + # https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:vacuum:0000A006:mijia-v1:1 + "battery": {"siid": 3, "piid": 1}, + "charge_state": {"siid": 3, "piid": 2}, + "error_code": {"siid": 2, "piid": 2}, + "state": {"siid": 2, "piid": 1}, + "fan_speed": {"siid": 2, "piid": 6}, + "operating_mode": {"siid": 2, "piid": 4}, + "mop_state": {"siid": 16, "piid": 1}, + "water_level": {"siid": 2, "piid": 5}, + "main_brush_life_level": {"siid": 14, "piid": 1}, + "main_brush_time_left": {"siid": 14, "piid": 2}, + "side_brush_life_level": {"siid": 15, "piid": 1}, + "side_brush_time_left": {"siid": 15, "piid": 2}, + "filter_life_level": {"siid": 11, "piid": 1}, + "filter_time_left": {"siid": 11, "piid": 2}, + "clean_area": {"siid": 9, "piid": 1}, + "clean_time": {"siid": 9, "piid": 2}, + # totals always return 0 + "total_clean_area": {"siid": 9, "piid": 3}, + "total_clean_time": {"siid": 9, "piid": 4}, + "total_clean_count": {"siid": 9, "piid": 5}, + "home": {"siid": 2, "aiid": 3}, + "find": {"siid": 6, "aiid": 1}, + "start": {"siid": 2, "aiid": 1}, + "stop": {"siid": 2, "aiid": 2}, + "reset_main_brush_life_level": {"siid": 14, "aiid": 1}, + "reset_side_brush_life_level": {"siid": 15, "aiid": 1}, + "reset_filter_life_level": {"siid": 11, "aiid": 1}, } +MIOT_MAPPING = {model: MAPPING for model in SUPPORTED_MODELS} + ERROR_CODES = { 0: "No error", 1: "Left Wheel stuck", @@ -277,9 +277,7 @@ def total_clean_time(self) -> timedelta: class G1Vacuum(MiotDevice): """Support for G1 vacuum (G1, mijia.vacuum.v2).""" - _supported_models = SUPPORTED_MODELS - - mapping = MIOT_MAPPING[MIJIA_VACUUM_V2] + _mappings = MIOT_MAPPING @command( default_output=format_output( diff --git a/miio/integrations/vacuum/roborock/tests/test_mirobo.py b/miio/integrations/vacuum/roborock/tests/test_mirobo.py index 68b8026de..81a22294a 100644 --- a/miio/integrations/vacuum/roborock/tests/test_mirobo.py +++ b/miio/integrations/vacuum/roborock/tests/test_mirobo.py @@ -6,6 +6,8 @@ def test_config_read(mocker): """Make sure config file is being read.""" x = mocker.patch("miio.integrations.vacuum.roborock.vacuum_cli._read_config") + mocker.patch("miio.device.Device.send") + runner = CliRunner() runner.invoke( cli, ["--ip", "127.0.0.1", "--token", "ffffffffffffffffffffffffffffffff"] diff --git a/miio/integrations/vacuum/roidmi/roidmivacuum_miot.py b/miio/integrations/vacuum/roidmi/roidmivacuum_miot.py index f27128404..d96275879 100644 --- a/miio/integrations/vacuum/roidmi/roidmivacuum_miot.py +++ b/miio/integrations/vacuum/roidmi/roidmivacuum_miot.py @@ -15,63 +15,65 @@ _LOGGER = logging.getLogger(__name__) -_MAPPING: MiotMapping = { - "battery_level": {"siid": 3, "piid": 1}, - "charging_state": {"siid": 3, "piid": 2}, - "error_code": {"siid": 2, "piid": 2}, - "state": {"siid": 2, "piid": 1}, - "filter_life_level": {"siid": 10, "piid": 1}, - "filter_left_minutes": {"siid": 10, "piid": 2}, - "main_brush_left_minutes": {"siid": 11, "piid": 1}, - "main_brush_life_level": {"siid": 11, "piid": 2}, - "side_brushes_left_minutes": {"siid": 12, "piid": 1}, - "side_brushes_life_level": {"siid": 12, "piid": 2}, - "sensor_dirty_time_left_minutes": { - "siid": 15, - "piid": 1, - }, # named brush_left_time in the spec - "sensor_dirty_remaning_level": {"siid": 15, "piid": 2}, - "sweep_mode": {"siid": 14, "piid": 1}, - "fanspeed_mode": {"siid": 2, "piid": 4}, - "sweep_type": {"siid": 2, "piid": 8}, - "path_mode": {"siid": 13, "piid": 8}, - "mop_present": {"siid": 8, "piid": 1}, - "work_station_freq": {"siid": 8, "piid": 2}, # Range: [0, 3, 1] - "timing": {"siid": 8, "piid": 6}, - "clean_area": {"siid": 8, "piid": 7}, # uint32 - # "uid": {"siid": 8, "piid": 8}, # str - This UID is unknown - "auto_boost": {"siid": 8, "piid": 9}, - "forbid_mode": {"siid": 8, "piid": 10}, # str - "water_level": {"siid": 8, "piid": 11}, - "total_clean_time_sec": {"siid": 8, "piid": 13}, - "total_clean_areas": {"siid": 8, "piid": 14}, - "clean_counts": {"siid": 8, "piid": 18}, - "clean_time_sec": {"siid": 8, "piid": 19}, - "double_clean": {"siid": 8, "piid": 20}, - # "edge_sweep": {"siid": 8, "piid": 21}, # 2021-07-11: Roidmi Eve is not changing behavior when this bool is changed - "led_switch": {"siid": 8, "piid": 22}, - "lidar_collision": {"siid": 8, "piid": 23}, - "station_key": {"siid": 8, "piid": 24}, - "station_led": {"siid": 8, "piid": 25}, - "current_audio": {"siid": 8, "piid": 26}, - # "progress": {"siid": 8, "piid": 28}, # 2021-07-11: this is part of the spec, but not implemented in Roidme Eve - "station_type": {"siid": 8, "piid": 29}, # uint32 - # "voice_conf": {"siid": 8, "piid": 30}, # Always return file not exist !!! - # "switch_status": {"siid": 2, "piid": 10}, # Enum with only one value: Open - "volume": {"siid": 9, "piid": 1}, - "mute": {"siid": 9, "piid": 2}, - "start": {"siid": 2, "aiid": 1}, - "stop": {"siid": 2, "aiid": 2}, - "start_room_sweep": {"siid": 2, "aiid": 3}, - "start_sweep": {"siid": 14, "aiid": 1}, - "home": {"siid": 3, "aiid": 1}, - "identify": {"siid": 8, "aiid": 1}, - "start_station_dust_collection": {"siid": 8, "aiid": 6}, - "set_voice": {"siid": 8, "aiid": 12}, - "reset_filter_life": {"siid": 10, "aiid": 1}, - "reset_main_brush_life": {"siid": 11, "aiid": 1}, - "reset_side_brushes_life": {"siid": 12, "aiid": 1}, - "reset_sensor_dirty_life": {"siid": 15, "aiid": 1}, +_MAPPINGS: MiotMapping = { + "roidmi.vacuum.v60": { + "battery_level": {"siid": 3, "piid": 1}, + "charging_state": {"siid": 3, "piid": 2}, + "error_code": {"siid": 2, "piid": 2}, + "state": {"siid": 2, "piid": 1}, + "filter_life_level": {"siid": 10, "piid": 1}, + "filter_left_minutes": {"siid": 10, "piid": 2}, + "main_brush_left_minutes": {"siid": 11, "piid": 1}, + "main_brush_life_level": {"siid": 11, "piid": 2}, + "side_brushes_left_minutes": {"siid": 12, "piid": 1}, + "side_brushes_life_level": {"siid": 12, "piid": 2}, + "sensor_dirty_time_left_minutes": { + "siid": 15, + "piid": 1, + }, # named brush_left_time in the spec + "sensor_dirty_remaning_level": {"siid": 15, "piid": 2}, + "sweep_mode": {"siid": 14, "piid": 1}, + "fanspeed_mode": {"siid": 2, "piid": 4}, + "sweep_type": {"siid": 2, "piid": 8}, + "path_mode": {"siid": 13, "piid": 8}, + "mop_present": {"siid": 8, "piid": 1}, + "work_station_freq": {"siid": 8, "piid": 2}, # Range: [0, 3, 1] + "timing": {"siid": 8, "piid": 6}, + "clean_area": {"siid": 8, "piid": 7}, # uint32 + # "uid": {"siid": 8, "piid": 8}, # str - This UID is unknown + "auto_boost": {"siid": 8, "piid": 9}, + "forbid_mode": {"siid": 8, "piid": 10}, # str + "water_level": {"siid": 8, "piid": 11}, + "total_clean_time_sec": {"siid": 8, "piid": 13}, + "total_clean_areas": {"siid": 8, "piid": 14}, + "clean_counts": {"siid": 8, "piid": 18}, + "clean_time_sec": {"siid": 8, "piid": 19}, + "double_clean": {"siid": 8, "piid": 20}, + # "edge_sweep": {"siid": 8, "piid": 21}, # 2021-07-11: Roidmi Eve is not changing behavior when this bool is changed + "led_switch": {"siid": 8, "piid": 22}, + "lidar_collision": {"siid": 8, "piid": 23}, + "station_key": {"siid": 8, "piid": 24}, + "station_led": {"siid": 8, "piid": 25}, + "current_audio": {"siid": 8, "piid": 26}, + # "progress": {"siid": 8, "piid": 28}, # 2021-07-11: this is part of the spec, but not implemented in Roidme Eve + "station_type": {"siid": 8, "piid": 29}, # uint32 + # "voice_conf": {"siid": 8, "piid": 30}, # Always return file not exist !!! + # "switch_status": {"siid": 2, "piid": 10}, # Enum with only one value: Open + "volume": {"siid": 9, "piid": 1}, + "mute": {"siid": 9, "piid": 2}, + "start": {"siid": 2, "aiid": 1}, + "stop": {"siid": 2, "aiid": 2}, + "start_room_sweep": {"siid": 2, "aiid": 3}, + "start_sweep": {"siid": 14, "aiid": 1}, + "home": {"siid": 3, "aiid": 1}, + "identify": {"siid": 8, "aiid": 1}, + "start_station_dust_collection": {"siid": 8, "aiid": 6}, + "set_voice": {"siid": 8, "aiid": 12}, + "reset_filter_life": {"siid": 10, "aiid": 1}, + "reset_main_brush_life": {"siid": 11, "aiid": 1}, + "reset_side_brushes_life": {"siid": 12, "aiid": 1}, + "reset_sensor_dirty_life": {"siid": 15, "aiid": 1}, + } } @@ -535,8 +537,7 @@ def sensor_dirty_left(self) -> timedelta: class RoidmiVacuumMiot(MiotDevice): """Interface for Vacuum Eve Plus (roidmi.vacuum.v60)""" - mapping = _MAPPING - _supported_models = ["roidmi.vacuum.v60"] + _mappings = _MAPPINGS @command() def status(self) -> RoidmiVacuumStatus: @@ -698,9 +699,9 @@ def disable_dnd(self): # The current do not disturb is read back for a better user expierence, # as start/end time must be set together with enabled=False try: - current_dnd_str = self.get_property_by(**_MAPPING["forbid_mode"])[0][ - "value" - ] + current_dnd_str = self.get_property_by( + **self._get_mapping()["forbid_mode"] + )[0]["value"] current_dnd_dict = json.loads(current_dnd_str) except Exception: # In case reading current DND back fails, DND is disabled anyway diff --git a/miio/miot_device.py b/miio/miot_device.py index 28cbcf669..43cafcc01 100644 --- a/miio/miot_device.py +++ b/miio/miot_device.py @@ -36,7 +36,7 @@ class MiotDevice(Device): remains in-place for backwards compatibility. """ - mapping: MiotMapping + mapping: MiotMapping # Deprecated, use _mappings instead _mappings: Dict[str, MiotMapping] = {} def __init__( diff --git a/miio/tests/test_device.py b/miio/tests/test_device.py index 10f826d4d..1221dc364 100644 --- a/miio/tests/test_device.py +++ b/miio/tests/test_device.py @@ -122,4 +122,4 @@ def test_device_supported_models(cls): if cls.__name__ == "MiotDevice": # skip miotdevice return - assert cls._supported_models + assert cls.supported_models diff --git a/miio/tests/test_miotdevice.py b/miio/tests/test_miotdevice.py index 7bfe8ddac..52787076a 100644 --- a/miio/tests/test_miotdevice.py +++ b/miio/tests/test_miotdevice.py @@ -1,8 +1,13 @@ import pytest -from miio import MiotDevice +from miio import Huizuo, MiotDevice from miio.miot_device import MiotValueType +MIOT_DEVICES = MiotDevice.__subclasses__() +# TODO: huizuo needs to be refactored to use _mappings, +# until then, just disable the tests on it. +MIOT_DEVICES.remove(Huizuo) # type: ignore + @pytest.fixture(scope="module") def dev(module_mocker): @@ -113,3 +118,36 @@ def test_get_mapping_backwards_compat(dev): # as dev is mocked on module level, need to empty manually dev._mappings = {} assert dev._get_mapping() == {} + + +@pytest.mark.parametrize("cls", MIOT_DEVICES) +def test_mapping_deprecation(cls): + """Check that deprecated mapping is not used.""" + # TODO: this can be removed in the future. + assert not hasattr(cls, "mapping") + + +@pytest.mark.parametrize("cls", MIOT_DEVICES) +def test_mapping_structure(cls): + """Check that mappings are structured correctly.""" + assert cls._mappings + + model, contents = next(iter(cls._mappings.items())) + + # model must contain a dot + assert "." in model + + method, piid_siid = next(iter(contents.items())) + assert isinstance(method, str) + + # mapping should be a dict with piid, siid + assert "piid" in piid_siid + assert "siid" in piid_siid + + +@pytest.mark.parametrize("cls", MIOT_DEVICES) +def test_supported_models(cls): + assert cls.supported_models == cls._mappings.keys() + + # make sure that that _supported_models is not defined + assert not cls._supported_models diff --git a/miio/yeelight_dual_switch.py b/miio/yeelight_dual_switch.py index b7a9c26e1..09c12cd7f 100644 --- a/miio/yeelight_dual_switch.py +++ b/miio/yeelight_dual_switch.py @@ -17,22 +17,30 @@ class Switch(enum.Enum): Second = 1 -_MAPPING: MiotMapping = { - # http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:switch:0000A003:yeelink-sw1:1:0000C809 - # First Switch (siid=2) - "switch_1_state": {"siid": 2, "piid": 1}, # bool - "switch_1_default_state": {"siid": 2, "piid": 2}, # 0 - Off, 1 - On - "switch_1_off_delay": {"siid": 2, "piid": 3}, # -1 - Off, [1, 43200] - delay in sec - # Second Switch (siid=3) - "switch_2_state": {"siid": 3, "piid": 1}, # bool - "switch_2_default_state": {"siid": 3, "piid": 2}, # 0 - Off, 1 - On - "switch_2_off_delay": {"siid": 3, "piid": 3}, # -1 - Off, [1, 43200] - delay in sec - # Extensions (siid=4) - "interlock": {"siid": 4, "piid": 1}, # bool - "flex_mode": {"siid": 4, "piid": 2}, # 0 - Off, 1 - On - "rc_list": {"siid": 4, "piid": 3}, # string - "rc_list_for_del": {"siid": 4, "piid": 4}, # string - "toggle": {"siid": 4, "piid": 5}, # 0 - First switch, 1 - Second switch +_MAPPINGS: MiotMapping = { + "yeelink.switch.sw1": { + # http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:switch:0000A003:yeelink-sw1:1:0000C809 + # First Switch (siid=2) + "switch_1_state": {"siid": 2, "piid": 1}, # bool + "switch_1_default_state": {"siid": 2, "piid": 2}, # 0 - Off, 1 - On + "switch_1_off_delay": { + "siid": 2, + "piid": 3, + }, # -1 - Off, [1, 43200] - delay in sec + # Second Switch (siid=3) + "switch_2_state": {"siid": 3, "piid": 1}, # bool + "switch_2_default_state": {"siid": 3, "piid": 2}, # 0 - Off, 1 - On + "switch_2_off_delay": { + "siid": 3, + "piid": 3, + }, # -1 - Off, [1, 43200] - delay in sec + # Extensions (siid=4) + "interlock": {"siid": 4, "piid": 1}, # bool + "flex_mode": {"siid": 4, "piid": 2}, # 0 - Off, 1 - On + "rc_list": {"siid": 4, "piid": 3}, # string + "rc_list_for_del": {"siid": 4, "piid": 4}, # string + "toggle": {"siid": 4, "piid": 5}, # 0 - First switch, 1 - Second switch + } } @@ -107,8 +115,7 @@ class YeelightDualControlModule(MiotDevice): """Main class representing the Yeelight Dual Control Module (yeelink.switch.sw1) which uses MIoT protocol.""" - mapping = _MAPPING - _supported_models = ["yeelink.switch.sw1"] + _mappings = _MAPPINGS @command( default_output=format_output( @@ -140,7 +147,7 @@ def status(self) -> DualControlModuleStatus: # Filter only readable properties for status properties = [ {"did": k, **v} - for k, v in filter(lambda item: item[0] in p, _MAPPING.items()) + for k, v in filter(lambda item: item[0] in p, self._get_mapping().items()) ] values = self.get_properties(properties) return DualControlModuleStatus(