From f1c98a0143624f88e2a9a76628d0b00296c9f2d2 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 20 Dec 2018 22:23:30 +0100 Subject: [PATCH 01/12] Add basic Xiaomi Air Condition support --- miio/__init__.py | 1 + miio/aircondition.py | 177 +++++++++++++++++++++++++++++++++++++++++++ miio/discovery.py | 4 +- 3 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 miio/aircondition.py diff --git a/miio/__init__.py b/miio/__init__.py index 14d9f2a6c..caed1015b 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa +from miio.aircondition import AirCondition from miio.airconditioningcompanion import AirConditioningCompanion from miio.airconditioningcompanion import AirConditioningCompanionV3 from miio.airfresh import AirFresh diff --git a/miio/aircondition.py b/miio/aircondition.py new file mode 100644 index 000000000..441adea1b --- /dev/null +++ b/miio/aircondition.py @@ -0,0 +1,177 @@ +import enum +import logging +from collections import defaultdict +from typing import Optional + +from .click_common import command, format_output +from .device import Device, DeviceException + +_LOGGER = logging.getLogger(__name__) + +MODEL_AIRCONDITION_MA1 = 'zhimi.aircondition.ma1' + +MODELS_SUPPORTED = [MODEL_AIRCONDITION_MA1] + + +class AirConditionException(DeviceException): + pass + + +class OperationMode(enum.Enum): + Arefaction = 'arefaction' + Cool = 'cooling' + Heat = 'heat' + Wind = 'wind' + + +class AirConditionStatus: + """Container for status reports of the Xiaomi Air Condition.""" + + def __init__(self, data): + """ + Device model: zhimi.aircondition.ma1 + + {'mode': 'cooling', + 'lcd_auto': 'off', + 'lcd_level': 1, + 'volume': 'off', + 'idle_timer': 0, + 'open_timer': 0, + 'power': 'on', + 'temp_dec': 244, + 'st_temp_dec': 320, + 'humidity': null, + 'speed_level': 5, + 'vertical_swing': 'on', + 'ptc': 'off', + 'ptc_rt': 'off', + 'silent': 'off', + 'vertical_end': 60, + 'vertical_rt': 19, + 'ele_quantity': null, + 'ex_humidity': null, + 'remote_mac': null, + 'htsensor_mac': null, + 'speed_level': 5, + 'vertical_swing': 'on', + 'ht_sensor': null, + 'comfort': 'off', + 'ot_run_temp': 7, + 'ep_temp': 27, + 'es_temp': 13, + 'he_temp': 39, + 'ot_humidity': null, + 'compressor_frq': 0, + 'motor_speed': 1000} + + """ + self.data = data + + @property + def power(self) -> str: + """Current power state.""" + return self.data['power'] + + @property + def is_on(self) -> bool: + """True if the device is turned on.""" + return self.power == 'on' + + @property + def temperature(self) -> int: + """Current temperature.""" + return self.data['temp_dec'] / 10.0 + + @property + def target_temperature(self) -> int: + """Target temperature.""" + return self.data['st_temp_dec'] / 10.0 + + @property + def mode(self) -> Optional[OperationMode]: + """Current operation mode.""" + try: + return OperationMode(self.data['mode']) + except TypeError: + return None + + def __repr__(self) -> str: + s = "" % \ + (self.power, + self.temperature, + self.target_temperature, + self.mode) + return s + + def __json__(self): + return self.data + + +class AirCondition(Device): + """Main class representing Xiaomi Air Condition MA1.""" + + def __init__(self, ip: str = None, token: str = None, start_id: int = 0, + debug: int = 0, lazy_discover: bool = True, + model: str = MODEL_AIRCONDITION_MA1) -> None: + super().__init__(ip, token, start_id, debug, lazy_discover) + + if model in MODELS_SUPPORTED: + self.model = model + else: + self.model = MODEL_AIRCONDITION_MA1 + _LOGGER.error("Device model %s unsupported. Falling back to %s.", model, self.model) + + @command( + default_output=format_output( + "", + "Power: {result.power}\n" + "Temperature: {result.temperature} °C\n" + "Target temperature: {result.target_temperature} °C\n" + "Mode: {result.mode}\n" + ) + ) + def status(self) -> AirConditionStatus: + """Retrieve properties.""" + + properties = ['mode', 'lcd_auto', 'lcd_level', 'volume', 'idle_timer', 'open_timer', 'power', 'temp_dec', + 'st_temp_dec', 'speed_level', 'vertical_swing', 'ptc', 'ptc_rt', 'silent', 'vertical_end', + 'vertical_rt', 'speed_level', 'vertical_swing', 'comfort', 'ot_run_temp', 'ep_temp', 'es_temp', + 'he_temp', 'compressor_frq', 'motor_speed', 'humidity', 'ele_quantity', 'ex_humidity', + 'remote_mac', 'htsensor_mac', 'ht_sensor', 'ot_humidity'] + + # A single request is limited to 16 properties. Therefore the + # properties are divided into multiple requests + _props = properties.copy() + values = [] + while _props: + values.extend(self.send("get_prop", _props[:15])) + _props[:] = _props[15:] + + properties_count = len(properties) + values_count = len(values) + if properties_count != values_count: + _LOGGER.debug( + "Count (%s) of requested properties does not match the " + "count (%s) of received values.", + properties_count, values_count) + + return AirConditionStatus( + defaultdict(lambda: None, zip(properties, values))) + + @command( + default_output=format_output("Powering the air condition on"), + ) + def on(self): + """Turn the air condition on.""" + return self.send("set_power", ["on"]) + + @command( + default_output=format_output("Powering the air condition off"), + ) + def off(self): + """Turn the air condition off.""" + return self.send("set_power", ["off"]) diff --git a/miio/discovery.py b/miio/discovery.py index 01086efba..a05cb2f85 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -10,7 +10,8 @@ from . import (Device, Vacuum, ChuangmiPlug, PowerStrip, AirPurifier, AirFresh, Ceil, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight, ChuangmiIr, AirHumidifier, WaterPurifier, WifiSpeaker, WifiRepeater, - Yeelight, Fan, Cooker, AirConditioningCompanion, AirQualityMonitor, AqaraCamera) + Yeelight, Fan, Cooker, AirCondition, AirConditioningCompanion, + AirQualityMonitor, AqaraCamera, ) from .airconditioningcompanion import (MODEL_ACPARTNER_V1, MODEL_ACPARTNER_V2, MODEL_ACPARTNER_V3, ) from .airhumidifier import (MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_V1, ) @@ -75,6 +76,7 @@ "zhimi-fan-v3": partial(Fan, model=MODEL_FAN_V3), "zhimi-fan-sa1": partial(Fan, model=MODEL_FAN_SA1), "zhimi-fan-za1": partial(Fan, model=MODEL_FAN_ZA1), + "zhimi-aircondition-ma1": AirCondition, "zhimi-airfresh-va2": AirFresh, "zhimi-airmonitor-v1": AirQualityMonitor, "lumi-gateway-": lambda x: other_package_info( From d5e398f09239a8bf630a86b3236afb70ec51d9d3 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 20 Dec 2018 22:30:50 +0100 Subject: [PATCH 02/12] Make hound happy --- miio/aircondition.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index 441adea1b..7ee83a6b7 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -137,11 +137,12 @@ def __init__(self, ip: str = None, token: str = None, start_id: int = 0, def status(self) -> AirConditionStatus: """Retrieve properties.""" - properties = ['mode', 'lcd_auto', 'lcd_level', 'volume', 'idle_timer', 'open_timer', 'power', 'temp_dec', - 'st_temp_dec', 'speed_level', 'vertical_swing', 'ptc', 'ptc_rt', 'silent', 'vertical_end', - 'vertical_rt', 'speed_level', 'vertical_swing', 'comfort', 'ot_run_temp', 'ep_temp', 'es_temp', - 'he_temp', 'compressor_frq', 'motor_speed', 'humidity', 'ele_quantity', 'ex_humidity', - 'remote_mac', 'htsensor_mac', 'ht_sensor', 'ot_humidity'] + properties = ['mode', 'lcd_auto', 'lcd_level', 'volume', 'idle_timer', 'open_timer', + 'power', 'temp_dec', 'st_temp_dec', 'speed_level', 'vertical_swing', + 'ptc', 'ptc_rt', 'silent', 'vertical_end', 'vertical_rt', 'speed_level', + 'vertical_swing', 'comfort', 'ot_run_temp', 'ep_temp', 'es_temp', + 'he_temp', 'compressor_frq', 'motor_speed', 'humidity', 'ele_quantity', + 'ex_humidity', 'remote_mac', 'htsensor_mac', 'ht_sensor', 'ot_humidity'] # A single request is limited to 16 properties. Therefore the # properties are divided into multiple requests From e92177afb1268d53216e5749ef13b371def500d0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 1 Jan 2019 21:39:30 +0100 Subject: [PATCH 03/12] Add all setter --- miio/aircondition.py | 150 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 5 deletions(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index 7ee83a6b7..a2acbc4fe 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -3,7 +3,7 @@ from collections import defaultdict from typing import Optional -from .click_common import command, format_output +from .click_common import command, format_output, EnumType from .device import Device, DeviceException _LOGGER = logging.getLogger(__name__) @@ -53,7 +53,6 @@ def __init__(self, data): 'remote_mac': null, 'htsensor_mac': null, 'speed_level': 5, - 'vertical_swing': 'on', 'ht_sensor': null, 'comfort': 'off', 'ot_run_temp': 7, @@ -140,9 +139,9 @@ def status(self) -> AirConditionStatus: properties = ['mode', 'lcd_auto', 'lcd_level', 'volume', 'idle_timer', 'open_timer', 'power', 'temp_dec', 'st_temp_dec', 'speed_level', 'vertical_swing', 'ptc', 'ptc_rt', 'silent', 'vertical_end', 'vertical_rt', 'speed_level', - 'vertical_swing', 'comfort', 'ot_run_temp', 'ep_temp', 'es_temp', - 'he_temp', 'compressor_frq', 'motor_speed', 'humidity', 'ele_quantity', - 'ex_humidity', 'remote_mac', 'htsensor_mac', 'ht_sensor', 'ot_humidity'] + 'comfort', 'ot_run_temp', 'ep_temp', 'es_temp', 'he_temp', 'compressor_frq', + 'motor_speed', 'humidity', 'ele_quantity', 'ex_humidity', 'remote_mac', + 'htsensor_mac', 'ht_sensor', 'ot_humidity'] # A single request is limited to 16 properties. Therefore the # properties are divided into multiple requests @@ -176,3 +175,144 @@ def on(self): def off(self): """Turn the air condition off.""" return self.send("set_power", ["off"]) + + @command( + click.argument("temperature", type=float), + default_output=format_output( + "Setting target temperature to {temperature} degrees") + ) + def set_temperature(self, temperature: float): + """Set target temperature.""" + return self.send("set_temperature", [int(temperature * 10)]) + + @command( + click.argument("speed", type=int), + default_output=format_output( + "Setting speed to {speed}") + ) + def set_speed(self, speed: int): + """Set speed.""" + if speed < 0 or speed > 5: + raise AirConditionException("Invalid speed level: %s", speed) + + return self.send("set_spd_level", [speed]) + + @command( + click.argument("start", type=int), + click.argument("stop", type=int), + default_output=format_output("Setting swing range from {start} degrees to {stop} degrees") + ) + def set_swing_range(self, start: int=0, stop: int=60): + """Set swing range.""" + if start < 0 or stop <= start: + raise AirConditionException("Invalid swing range: %s %s", start, stop) + + return self.send("set_ver_range", [start, stop]) + + @command( + click.argument("swing", type=bool), + default_output=format_output( + lambda swing: "Turning on swing mode" + if swing else "Turning off swing mode" + ) + ) + def set_swing(self, swing: bool): + """Set swing on/off.""" + if swing: + return self.send("set_vertical", ["on"]) + else: + return self.send("set_vertical", ["off"]) + + @command( + click.argument("direction", type=int), + default_output=format_output( + "Setting wind direction to {direction} degree") + ) + def set_wind_direction(self, direction: int): + """Set wind direction.""" + return self.send("set_ver_pos", [direction]) + + @command( + click.argument("comfort", type=bool), + default_output=format_output( + lambda comfort: "Turning on comfort mode" + if comfort else "Turning off comfort mode" + ) + ) + def set_comfort(self, comfort: bool): + """Set comfort on/off.""" + if comfort: + return self.send("set_comfort", ["on"]) + else: + return self.send("set_comfort", ["off"]) + + @command( + click.argument("silent", type=bool), + default_output=format_output( + lambda silent: "Turning on silent mode" + if silent else "Turning off silent mode" + ) + ) + def set_silent(self, silent: bool): + """Set silent on/off.""" + if silent: + return self.send("set_silent", ["on"]) + else: + return self.send("set_silent", ["off"]) + + @command( + click.argument("ptc", type=bool), + default_output=format_output( + lambda ptc: "Turning on ptc mode" + if ptc else "Turning off ptc mode" + ) + ) + def set_ptc(self, ptc: bool): + """Set ptc on/off.""" + if ptc: + return self.send("set_ptc", ["on"]) + else: + return self.send("set_ptc", ["off"]) + + @command( + click.argument("volume", type=bool), + default_output=format_output( + lambda ptc: "Turning on ptc mode" + if ptc else "Turning off ptc mode" + ) + ) + def set_mute_audio(self, mute: bool): + """Mute audio on/off.""" + if mute: + return self.send("set_volume_sw", ["off"]) + else: + return self.send("set_volume_sw", ["on"]) + + @command( + click.argument("brightness", type=int), + default_output=format_output( + "Setting LCD brightness to {brightness}") + ) + def set_lcd_brightness(self, brightness: int): + """Set lcd brightness.""" + if brightness < 0 or brightness > 5: + raise AirConditionException("Invalid LCD brightness: %s", brightness) + + return self.send("set_lcd", [brightness]) + + @command( + click.argument("mode", type=EnumType(OperationMode, False)), + default_output=format_output("Setting mode to '{mode.value}'") + ) + def set_mode(self, mode: OperationMode): + """Set mode.""" + return self.send("set_mode", [mode.value]) + + @command( + click.argument("seconds", type=int), + default_output=format_output( + "Setting idle timer to {seconds} seconds") + ) + def set_idle_timer(self, seconds: int): + """Set idle timer.""" + return self.send("set_idle_timer", [seconds]) From be6edd5b537fc720cc674f845b263e3421cf5109 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 1 Jan 2019 21:40:47 +0100 Subject: [PATCH 04/12] Add missing import --- miio/aircondition.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miio/aircondition.py b/miio/aircondition.py index a2acbc4fe..31aa15dcb 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -3,6 +3,8 @@ from collections import defaultdict from typing import Optional +import click + from .click_common import command, format_output, EnumType from .device import Device, DeviceException From 86785a5d0ed7607cd71d29bdb26776bc21265721 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 1 Jan 2019 21:45:20 +0100 Subject: [PATCH 05/12] Improve wording --- miio/aircondition.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index 31aa15dcb..32bef12d5 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -293,28 +293,31 @@ def set_mute_audio(self, mute: bool): @command( click.argument("brightness", type=int), default_output=format_output( - "Setting LCD brightness to {brightness}") + "Setting display brightness to {brightness}") ) - def set_lcd_brightness(self, brightness: int): - """Set lcd brightness.""" + def set_display_brightness(self, brightness: int): + """Set display brightness.""" if brightness < 0 or brightness > 5: - raise AirConditionException("Invalid LCD brightness: %s", brightness) + raise AirConditionException("Invalid display brightness: %s", brightness) return self.send("set_lcd", [brightness]) @command( click.argument("mode", type=EnumType(OperationMode, False)), - default_output=format_output("Setting mode to '{mode.value}'") + default_output=format_output("Setting operation mode to '{mode.value}'") ) def set_mode(self, mode: OperationMode): - """Set mode.""" + """Set operation mode.""" return self.send("set_mode", [mode.value]) @command( click.argument("seconds", type=int), - default_output=format_output( - "Setting idle timer to {seconds} seconds") + default_output=format_output("Setting delayed turn off to {seconds} seconds") ) - def set_idle_timer(self, seconds: int): - """Set idle timer.""" + def delay_off(self, seconds: int): + """Set delay off seconds.""" + if seconds < 1: + raise AirConditionException( + "Invalid value for a delayed turn off: %s" % seconds) + return self.send("set_idle_timer", [seconds]) From 145f78bb3ce1c634793916a755b09d9e9fd381bc Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 1 Jan 2019 21:50:13 +0100 Subject: [PATCH 06/12] Catch edge cases --- miio/aircondition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index 32bef12d5..f933bac2d 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -206,7 +206,7 @@ def set_speed(self, speed: int): ) def set_swing_range(self, start: int=0, stop: int=60): """Set swing range.""" - if start < 0 or stop <= start: + if start < 0 or stop <= start or start > 60 or stop > 60: raise AirConditionException("Invalid swing range: %s %s", start, stop) return self.send("set_ver_range", [start, stop]) From 47859b959df8d5f52a4dd896bfe00efc2a309c63 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 2 Jan 2019 20:22:50 +0100 Subject: [PATCH 07/12] Add minor fixes --- miio/aircondition.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index f933bac2d..8144ec10d 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -228,10 +228,13 @@ def set_swing(self, swing: bool): @command( click.argument("direction", type=int), default_output=format_output( - "Setting wind direction to {direction} degree") + "Setting wind direction to {direction} degrees") ) def set_wind_direction(self, direction: int): """Set wind direction.""" + if direction < 0 or direction > 60: + raise AirConditionException("Invalid wind direction: %s", direction) + return self.send("set_ver_pos", [direction]) @command( @@ -277,18 +280,18 @@ def set_ptc(self, ptc: bool): return self.send("set_ptc", ["off"]) @command( - click.argument("volume", type=bool), + click.argument("audio", type=bool), default_output=format_output( - lambda ptc: "Turning on ptc mode" - if ptc else "Turning off ptc mode" + lambda ptc: "Turning on audio" + if ptc else "Turning off audio" ) ) - def set_mute_audio(self, mute: bool): - """Mute audio on/off.""" - if mute: - return self.send("set_volume_sw", ["off"]) - else: + def set_audio(self, audio: bool): + """Turn audio on/off.""" + if audio: return self.send("set_volume_sw", ["on"]) + else: + return self.send("set_volume_sw", ["off"]) @command( click.argument("brightness", type=int), From 9de322174b3c73771b96d0cbde943e74e33a7c0d Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 2 Jan 2019 21:16:12 +0100 Subject: [PATCH 08/12] Add properties, some setter and two additional hw models --- miio/aircondition.py | 206 ++++++++++++++++++++++++++++++++++++++----- miio/discovery.py | 5 +- 2 files changed, 188 insertions(+), 23 deletions(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index 8144ec10d..4865e3f28 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -11,8 +11,10 @@ _LOGGER = logging.getLogger(__name__) MODEL_AIRCONDITION_MA1 = 'zhimi.aircondition.ma1' +MODEL_AIRCONDITION_MA2 = 'zhimi.aircondition.ma2' +MODEL_AIRCONDITION_SA1 = 'zhimi.aircondition.sa1' -MODELS_SUPPORTED = [MODEL_AIRCONDITION_MA1] +MODELS_SUPPORTED = [MODEL_AIRCONDITION_MA1, MODEL_AIRCONDITION_MA2, MODEL_AIRCONDITION_SA1] class AirConditionException(DeviceException): @@ -42,28 +44,27 @@ def __init__(self, data): 'power': 'on', 'temp_dec': 244, 'st_temp_dec': 320, - 'humidity': null, - 'speed_level': 5, 'vertical_swing': 'on', 'ptc': 'off', 'ptc_rt': 'off', 'silent': 'off', 'vertical_end': 60, 'vertical_rt': 19, - 'ele_quantity': null, - 'ex_humidity': null, - 'remote_mac': null, - 'htsensor_mac': null, 'speed_level': 5, - 'ht_sensor': null, 'comfort': 'off', 'ot_run_temp': 7, 'ep_temp': 27, 'es_temp': 13, 'he_temp': 39, - 'ot_humidity': null, 'compressor_frq': 0, - 'motor_speed': 1000} + 'motor_speed': 1000, + 'humidity': null, + 'ele_quantity': null, + 'ex_humidity': null, + 'remote_mac': null, + 'htsensor_mac': null, + 'ht_sensor': null, + 'ot_humidity': null} """ self.data = data @@ -96,6 +97,80 @@ def mode(self) -> Optional[OperationMode]: except TypeError: return None + @property + def lcd_auto(self) -> bool: + """Lcd auto?.""" + return self.data['lcd_auto'] == 'on' + + @property + def display_brightness(self) -> int: + """Display brightness.""" + return self.data['lcd_level'] + + @property + def audio(self) -> bool: + """Audio output.""" + return self.data['volume'] == 'on' + + @property + def swing(self) -> bool: + """Swing.""" + return self.data['vertical_swing'] == 'on' + + @property + def electric_auxiliary_heat(self) -> bool: + """Electric auxiliary heat.""" + return self.data['ptc'] == 'on' + + @property + def ptc_rt(self) -> bool: + """Ptc rt?""" + return self.data['ptc'] == 'on' + + @property + def silent(self) -> bool: + """Silent mode.""" + return self.data['silent'] == 'on' + + @property + def comfort(self) -> bool: + """Comfort mode.""" + return self.data['comfort'] == 'on' + + @property + def delay_off(self) -> int: + """Delayed turn off.""" + return self.data['idle_timer'] + + @property + def open_timer(self) -> int: + """Open timer?.""" + return self.data['open_timer'] + + @property + def speed(self) -> int: + """Speed level.""" + return self.data['speed_level'] + + @property + def compressor_frequency(self) -> int: + """Compressor frequency.""" + return self.data['compressor_frq'] + + @property + def motor_speed(self) -> int: + """Motor speed.""" + return self.data['motor_speed'] + + @property + def swing_range(self) -> [int, int]: + """Swing range.""" + return [self.data['vertical_rt'], self.data['vertical_end']] + + @property + def extra_features(self) -> Optional[int]: + return self.data["app_extra"] + def __repr__(self) -> str: s = " AirConditionStatus: """Retrieve properties.""" - properties = ['mode', 'lcd_auto', 'lcd_level', 'volume', 'idle_timer', 'open_timer', - 'power', 'temp_dec', 'st_temp_dec', 'speed_level', 'vertical_swing', - 'ptc', 'ptc_rt', 'silent', 'vertical_end', 'vertical_rt', 'speed_level', - 'comfort', 'ot_run_temp', 'ep_temp', 'es_temp', 'he_temp', 'compressor_frq', - 'motor_speed', 'humidity', 'ele_quantity', 'ex_humidity', 'remote_mac', - 'htsensor_mac', 'ht_sensor', 'ot_humidity'] + properties = [ + 'mode', + 'lcd_auto', + 'lcd_level', + 'volume', + 'idle_timer', + 'open_timer', + 'power', + 'temp_dec', + 'st_temp_dec', + 'vertical_swing', + 'ptc', + 'ptc_rt', + 'silent', + 'vertical_end', + 'vertical_rt', + 'speed_level', + 'comfort', + 'ot_run_temp', + 'ep_temp', + 'es_temp', + 'he_temp', + 'compressor_frq', + 'motor_speed', + 'app_extra', + 'humidity', + 'ele_quantity', + 'ex_humidity', + 'remote_mac', + 'htsensor_mac', + 'ht_sensor', + 'ot_humidity', + ] # A single request is limited to 16 properties. Therefore the # properties are divided into multiple requests @@ -199,6 +301,21 @@ def set_speed(self, speed: int): return self.send("set_spd_level", [speed]) + @command( + click.argument("high", type=bool), + default_output=format_output( + lambda ptc: "Turning on high speed" + if ptc else "Turning off high speed" + ) + ) + def set_speed_high(self, high: bool): + """Turn automatic display brightness on/off.""" + if high: + return self.send("set_spd_high", ["on"]) + else: + return self.send("set_spd_high", ["off"]) + + @command( click.argument("start", type=int), click.argument("stop", type=int), @@ -266,15 +383,15 @@ def set_silent(self, silent: bool): return self.send("set_silent", ["off"]) @command( - click.argument("ptc", type=bool), + click.argument("power", type=bool), default_output=format_output( - lambda ptc: "Turning on ptc mode" - if ptc else "Turning off ptc mode" + lambda ptc: "Turning on electric auxiliary heat" + if ptc else "Turning off electric auxiliary heat" ) ) - def set_ptc(self, ptc: bool): - """Set ptc on/off.""" - if ptc: + def set_electric_auxiliary_heat(self, power: bool): + """Set electric auxiliary heat on/off.""" + if power: return self.send("set_ptc", ["on"]) else: return self.send("set_ptc", ["off"]) @@ -305,6 +422,20 @@ def set_display_brightness(self, brightness: int): return self.send("set_lcd", [brightness]) + @command( + click.argument("auto", type=bool), + default_output=format_output( + lambda ptc: "Turning on auto display brightness" + if ptc else "Turning off auto display brightness" + ) + ) + def set_auto_display_brightess(self, auto: bool): + """Turn automatic display brightness on/off.""" + if auto: + return self.send("set_lcd_auto", ["on"]) + else: + return self.send("set_lcd_auto", ["off"]) + @command( click.argument("mode", type=EnumType(OperationMode, False)), default_output=format_output("Setting operation mode to '{mode.value}'") @@ -324,3 +455,34 @@ def delay_off(self, seconds: int): "Invalid value for a delayed turn off: %s" % seconds) return self.send("set_idle_timer", [seconds]) + + @command( + click.argument("seconds", type=int), + default_output=format_output("Setting open timer to {seconds} seconds") + ) + def set_open_timer(self, seconds: int): + """Set open timer.""" + if seconds < 1: + raise AirConditionException( + "Invalid open timer value: %s" % seconds) + + return self.send("set_open_timer", [seconds]) + + @command( + click.argument("value", type=int), + default_output=format_output("Setting extra to {value}") + ) + def set_extra_features(self, value: int): + """Storage register to enable extra features at the app.""" + if value < 0: + raise AirConditionException("Invalid app extra value: %s", value) + + return self.send("set_app_extra", [value]) + + @command( + click.argument("value", type=int), + default_output=format_output("Setting humidity/temperature sensor to {value}") + ) + def set_ht_sensor(self, value: int): + """Set humidity/temperature sensor.""" + return self.send("set_ht_sensor", [value]) diff --git a/miio/discovery.py b/miio/discovery.py index a05cb2f85..a812f1c4b 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -13,6 +13,7 @@ Yeelight, Fan, Cooker, AirCondition, AirConditioningCompanion, AirQualityMonitor, AqaraCamera, ) +from .aircondition import (MODEL_AIRCONDITION_MA1, MODEL_AIRCONDITION_MA2, MODEL_AIRCONDITION_SA1) from .airconditioningcompanion import (MODEL_ACPARTNER_V1, MODEL_ACPARTNER_V2, MODEL_ACPARTNER_V3, ) from .airhumidifier import (MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_V1, ) from .chuangmi_plug import (MODEL_CHUANGMI_PLUG_V1, MODEL_CHUANGMI_PLUG_V3, @@ -76,7 +77,9 @@ "zhimi-fan-v3": partial(Fan, model=MODEL_FAN_V3), "zhimi-fan-sa1": partial(Fan, model=MODEL_FAN_SA1), "zhimi-fan-za1": partial(Fan, model=MODEL_FAN_ZA1), - "zhimi-aircondition-ma1": AirCondition, + "zhimi-aircondition-ma1": partial(AirCondition, model=MODEL_AIRCONDITION_MA1), + "zhimi-aircondition-ma2": partial(AirCondition, model=MODEL_AIRCONDITION_MA2), + "zhimi-aircondition-sa1": partial(AirCondition, model=MODEL_AIRCONDITION_SA1), "zhimi-airfresh-va2": AirFresh, "zhimi-airmonitor-v1": AirQualityMonitor, "lumi-gateway-": lambda x: other_package_info( From 719db1442ee6d0705d31f72711fc81996fbe4d8e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 2 Jan 2019 21:18:38 +0100 Subject: [PATCH 09/12] Set speed high isn't used right now --- miio/aircondition.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index 4865e3f28..d3be631bf 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -301,21 +301,6 @@ def set_speed(self, speed: int): return self.send("set_spd_level", [speed]) - @command( - click.argument("high", type=bool), - default_output=format_output( - lambda ptc: "Turning on high speed" - if ptc else "Turning off high speed" - ) - ) - def set_speed_high(self, high: bool): - """Turn automatic display brightness on/off.""" - if high: - return self.send("set_spd_high", ["on"]) - else: - return self.send("set_spd_high", ["off"]) - - @command( click.argument("start", type=int), click.argument("stop", type=int), From f10b77db631f3284ae36cf9479238266a7c809de Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 2 Jan 2019 21:36:55 +0100 Subject: [PATCH 10/12] Add operation mode "auto" --- miio/aircondition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/miio/aircondition.py b/miio/aircondition.py index d3be631bf..889cf1a0d 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -26,6 +26,7 @@ class OperationMode(enum.Enum): Cool = 'cooling' Heat = 'heat' Wind = 'wind' + Auto = 'automode' class AirConditionStatus: From 1332aa2070ae57e4ab5da6fdaee5c7e59784708e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 2 Jan 2019 21:51:26 +0100 Subject: [PATCH 11/12] Fix cli output, improve set_sensor and properties --- miio/aircondition.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index 889cf1a0d..e97892874 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -90,6 +90,15 @@ def target_temperature(self) -> int: """Target temperature.""" return self.data['st_temp_dec'] / 10.0 + @property + def humidity(self) -> Optional[int]: + """Current humidity.""" + return self.data["humidity"] + + def external_humidity(self) -> Optional[int]: + """Current external humidity.""" + return self.data["ex_humidity"] + @property def mode(self) -> Optional[OperationMode]: """Current operation mode.""" @@ -371,8 +380,8 @@ def set_silent(self, silent: bool): @command( click.argument("power", type=bool), default_output=format_output( - lambda ptc: "Turning on electric auxiliary heat" - if ptc else "Turning off electric auxiliary heat" + lambda power: "Turning on electric auxiliary heat" + if power else "Turning off electric auxiliary heat" ) ) def set_electric_auxiliary_heat(self, power: bool): @@ -385,8 +394,8 @@ def set_electric_auxiliary_heat(self, power: bool): @command( click.argument("audio", type=bool), default_output=format_output( - lambda ptc: "Turning on audio" - if ptc else "Turning off audio" + lambda audio: "Turning on audio" + if audio else "Turning off audio" ) ) def set_audio(self, audio: bool): @@ -411,8 +420,8 @@ def set_display_brightness(self, brightness: int): @command( click.argument("auto", type=bool), default_output=format_output( - lambda ptc: "Turning on auto display brightness" - if ptc else "Turning off auto display brightness" + lambda auto: "Turning on auto display brightness" + if auto else "Turning off auto display brightness" ) ) def set_auto_display_brightess(self, auto: bool): @@ -466,9 +475,15 @@ def set_extra_features(self, value: int): return self.send("set_app_extra", [value]) @command( - click.argument("value", type=int), - default_output=format_output("Setting humidity/temperature sensor to {value}") + click.argument("sensor", type=bool), + default_output=format_output( + lambda sensor: "Turning on humidity/temperature sensor" + if sensor else "Turning off humidity/temperature sensor" + ) ) - def set_ht_sensor(self, value: int): - """Set humidity/temperature sensor.""" - return self.send("set_ht_sensor", [value]) + def set_sensor(self, sensor: bool): + """Turn automatic display brightness on/off.""" + if sensor: + return self.send("set_ht_sensor", ["on"]) + else: + return self.send("set_ht_sensor", ["off"]) From 99417af5f3dc7c50407242a691e672464b0ab8b5 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 2 Jan 2019 22:09:13 +0100 Subject: [PATCH 12/12] Improve properties --- miio/aircondition.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/miio/aircondition.py b/miio/aircondition.py index e97892874..ad0117c11 100644 --- a/miio/aircondition.py +++ b/miio/aircondition.py @@ -85,6 +85,26 @@ def temperature(self) -> int: """Current temperature.""" return self.data['temp_dec'] / 10.0 + @property + def ot_run_temperature(self) -> int: + """Outdoor operation temperature.""" + return self.data['ot_run_temp'] + + @property + def ep_temperature(self) -> int: + """Outer ring temperature.""" + return self.data['ep_temp'] + + @property + def es_temperature(self) -> int: + """Outer ring temperature.""" + return self.data['es_temp'] + + @property + def he_temperature(self) -> int: + """Outlet temperature.""" + return self.data['he_temp'] + @property def target_temperature(self) -> int: """Target temperature.""" @@ -92,13 +112,18 @@ def target_temperature(self) -> int: @property def humidity(self) -> Optional[int]: - """Current humidity.""" + """Current indoor humidity.""" return self.data["humidity"] def external_humidity(self) -> Optional[int]: """Current external humidity.""" return self.data["ex_humidity"] + @property + def outdoor_humidity(self) -> Optional[int]: + """Current outdoor humidity.""" + return self.data["ot_humidity"] + @property def mode(self) -> Optional[OperationMode]: """Current operation mode.""" @@ -109,7 +134,7 @@ def mode(self) -> Optional[OperationMode]: @property def lcd_auto(self) -> bool: - """Lcd auto?.""" + """Automatic display brightness.""" return self.data['lcd_auto'] == 'on' @property @@ -124,7 +149,7 @@ def audio(self) -> bool: @property def swing(self) -> bool: - """Swing.""" + """Vertical swing.""" return self.data['vertical_swing'] == 'on' @property @@ -164,7 +189,7 @@ def speed(self) -> int: @property def compressor_frequency(self) -> int: - """Compressor frequency.""" + """Compressor frequency in Hz.""" return self.data['compressor_frq'] @property @@ -177,6 +202,11 @@ def swing_range(self) -> [int, int]: """Swing range.""" return [self.data['vertical_rt'], self.data['vertical_end']] + @property + def power_consumption(self) -> float: + """Power consumption in kWh.""" + return self.data['ele_quantity'] / 10.0 + @property def extra_features(self) -> Optional[int]: return self.data["app_extra"]