Source code for miio.integrations.lumi.acpartner.airconditioningcompanion

import enum
import logging
from typing import Optional

import click

from miio import Device, DeviceStatus
from miio.click_common import EnumType, command, format_output

_LOGGER = logging.getLogger(__name__)

MODEL_ACPARTNER_V1 = "lumi.acpartner.v1"
MODEL_ACPARTNER_V2 = "lumi.acpartner.v2"
MODEL_ACPARTNER_V3 = "lumi.acpartner.v3"

MODELS_SUPPORTED = [MODEL_ACPARTNER_V1, MODEL_ACPARTNER_V2, MODEL_ACPARTNER_V3]


[docs] class OperationMode(enum.Enum): Heat = 0 Cool = 1 Auto = 2 Dehumidify = 3 Ventilate = 4
[docs] class FanSpeed(enum.Enum): Low = 0 Medium = 1 High = 2 Auto = 3
[docs] class SwingMode(enum.Enum): On = "0" Off = "1" Unknown2 = "2" Unknown7 = "7" ChigoOn = "C" ChigoOff = "D"
[docs] class Power(enum.Enum): On = 1 Off = 0
[docs] class Led(enum.Enum): On = "0" Off = "A"
STORAGE_SLOT_ID = 30 POWER_OFF = "off" # Command templates per model number (f.e. 0180111111) # [po], [mo], [wi], [sw], [tt], [tt1], [tt4] and [tt7] are markers which will be replaced DEVICE_COMMAND_TEMPLATES = { "fallback": {"deviceType": "generic", "base": "[po][mo][wi][sw][tt][li]"}, "0100010727": { "deviceType": "gree_2", "base": "[po][mo][wi][sw][tt]1100190[tt1]205002102000[tt7]0190[tt1]207002000000[tt4]", "off": "01011101004000205002112000D04000207002000000A0", }, "0100004795": { "deviceType": "gree_8", "base": "[po][mo][wi][sw][tt][li]10009090000500", }, "0180333331": {"deviceType": "haier_1", "base": "[po][mo][wi][sw][tt]1"}, "0180666661": {"deviceType": "aux_1", "base": "[po][mo][wi][sw][tt]1"}, "0180777771": {"deviceType": "chigo_1", "base": "[po][mo][wi][sw][tt]1"}, }
[docs] class AirConditioningCompanionStatus(DeviceStatus): """Container for status reports of the Xiaomi AC Companion.""" def __init__(self, data): """Device model: lumi.acpartner.v2. Response of "get_model_and_state": ['010500978022222102', '010201190280222221', '2'] AC turned on by set_power=on: ['010507950000257301', '011001160100002573', '807'] AC turned off by set_power=off: ['010507950000257301', '010001160100002573', '6'] ... ['010507950000257301', '010001160100002573', '1'] Example data payload: { 'model_and_state': ['010500978022222102', '010201190280222221', '2'], 'power_socket': 'on' } """ self.data = data self.model = data["model_and_state"][0] self.state = data["model_and_state"][1] @property def load_power(self) -> int: """Current power load of the air conditioner.""" return int(self.data["model_and_state"][2]) @property def power_socket(self) -> Optional[str]: """Current socket power state.""" if "power_socket" in self.data and self.data["power_socket"] is not None: return self.data["power_socket"] return None @property def air_condition_model(self) -> bytes: """Model of the air conditioner.""" return bytes.fromhex(self.model) @property def model_format(self) -> int: """Version number of the model format.""" return self.air_condition_model[0] @property def device_type(self) -> int: """Device type identifier.""" return self.air_condition_model[1] @property def air_condition_brand(self) -> int: """Brand of the air conditioner. Known brand ids are 0x0182, 0x0097, 0x0037, 0x0202, 0x02782, 0x0197, 0x0192. """ return int(self.air_condition_model[2:4].hex(), 16) @property def air_condition_remote(self) -> int: """Remote id. Known remote ids: * 0x80111111, 0x80111112 (brand: 0x0182) * 0x80222221 (brand: 0x0097) * 0x80333331 (brand: 0x0037) * 0x80444441 (brand: 0x0202) * 0x80555551 (brand: 0x2782) * 0x80777771 (brand: 0x0197) * 0x80666661 (brand: 0x0192) """ return int(self.air_condition_model[4:8].hex(), 16) @property def state_format(self) -> int: """Version number of the state format. Known values are: 1, 2, 3 """ return int(self.air_condition_model[8]) @property def air_condition_configuration(self) -> int: return self.state[2:10] @property def power(self) -> str: """Current power state.""" return "on" if int(self.state[2:3]) == Power.On.value else "off" @property def led(self) -> Optional[bool]: """Current LED state.""" state = self.state[8:9] if state == Led.On.value: return True if state == Led.Off.value: return False _LOGGER.info("Unsupported LED state: %s", state) return None @property def is_on(self) -> bool: """True if the device is turned on.""" return self.power == "on" @property def target_temperature(self) -> Optional[int]: """Target temperature.""" try: return int(self.state[6:8], 16) except TypeError: return None @property def swing_mode(self) -> Optional[SwingMode]: """Current swing mode.""" try: mode = self.state[5:6] return SwingMode(mode) except TypeError: return None @property def fan_speed(self) -> Optional[FanSpeed]: """Current fan speed.""" try: speed = int(self.state[4:5]) return FanSpeed(speed) except TypeError: return None @property def mode(self) -> Optional[OperationMode]: """Current operation mode.""" try: mode = int(self.state[3:4]) return OperationMode(mode) except TypeError: return None
[docs] class AirConditioningCompanion(Device): """Main class representing Xiaomi Air Conditioning Companion V1 and V2.""" _supported_models = MODELS_SUPPORTED def __init__( self, ip: Optional[str] = None, token: Optional[str] = None, start_id: int = 0, debug: int = 0, lazy_discover: bool = True, timeout: Optional[int] = None, model: str = MODEL_ACPARTNER_V2, ) -> None: super().__init__( ip, token, start_id, debug, lazy_discover, timeout=timeout, model=model ) if self.model not in MODELS_SUPPORTED: _LOGGER.error( "Device model %s unsupported. Falling back to %s.", model, self.model )
[docs] @command( default_output=format_output( "", "Power: {result.power}\n" "Load power: {result.load_power}\n" "Air Condition model: {result.air_condition_model}\n" "LED: {result.led}\n" "Target temperature: {result.target_temperature} °C\n" "Swing mode: {result.swing_mode}\n" "Fan speed: {result.fan_speed}\n" "Mode: {result.mode}\n", ) ) def status(self) -> AirConditioningCompanionStatus: """Return device status.""" status = self.send("get_model_and_state") return AirConditioningCompanionStatus(dict(model_and_state=status))
[docs] @command(default_output=format_output("Powering the air condition on")) def on(self): """Turn the air condition on by infrared.""" return self.send("set_power", ["on"])
[docs] @command(default_output=format_output("Powering the air condition off")) def off(self): """Turn the air condition off by infrared.""" return self.send("set_power", ["off"])
[docs] @command( click.argument("slot", type=int), default_output=format_output( "Learning infrared command into storage slot {slot}" ), ) def learn(self, slot: int = STORAGE_SLOT_ID): """Learn an infrared command.""" return self.send("start_ir_learn", [slot])
[docs] @command(default_output=format_output("Reading learned infrared commands")) def learn_result(self): """Read the learned command.""" return self.send("get_ir_learn_result")
[docs] @command( click.argument("slot", type=int), default_output=format_output( "Learning infrared command into storage slot {slot} stopped" ), ) def learn_stop(self, slot: int = STORAGE_SLOT_ID): """Stop learning of a infrared command.""" return self.send("end_ir_learn", [slot])
[docs] @command( click.argument("model", type=str), click.argument("code", type=str), default_output=format_output("Sending the supplied infrared command"), ) def send_ir_code(self, model: str, code: str, slot: int = 0): """Play a captured command. :param str model: Air condition model :param str code: Command to execute :param int slot: Unknown internal register or slot """ try: model_bytes = bytes.fromhex(model) except ValueError: raise ValueError("Invalid model. A hexadecimal string must be provided") try: code_bytes = bytes.fromhex(code) except ValueError: raise ValueError("Invalid code. A hexadecimal string must be provided") if slot < 0 or slot > 134: raise ValueError("Invalid slot: %s" % slot) slot_bytes = bytes([121 + slot]) # FE + 0487 + 00007145 + 9470 + 1FFF + 7F + FF + 06 + 0042 + 27 + 4E + 0025002D008500AC01... command_bytes = ( code_bytes[0:1] + model_bytes[2:8] + b"\x94\x70\x1F\xFF" + slot_bytes + b"\xFF" + code_bytes[13:16] + b"\x27" ) checksum = sum(command_bytes) & 0xFF command_bytes = command_bytes + bytes([checksum]) + code_bytes[18:] return self.send("send_ir_code", [command_bytes.hex().upper()])
[docs] @command( click.argument("command", type=str), default_output=format_output("Sending a command to the air conditioner"), ) def send_command(self, command: str): """Send a command to the air conditioner. :param str command: Command to execute """ return self.send("send_cmd", [str(command)])
[docs] @command( click.argument("model", type=str), click.argument("power", type=EnumType(Power)), click.argument("operation_mode", type=EnumType(OperationMode)), click.argument("target_temperature", type=int), click.argument("fan_speed", type=EnumType(FanSpeed)), click.argument("swing_mode", type=EnumType(SwingMode)), click.argument("led", type=EnumType(Led)), default_output=format_output("Sending a configuration to the air conditioner"), ) def send_configuration( self, model: str, power: Power, operation_mode: OperationMode, target_temperature: int, fan_speed: FanSpeed, swing_mode: SwingMode, led: Led, ): prefix = str(model[0:2] + model[8:16]) suffix = model[-1:] # Static turn off command available? if ( (power is Power.Off) and (prefix in DEVICE_COMMAND_TEMPLATES) and (POWER_OFF in DEVICE_COMMAND_TEMPLATES[prefix]) ): return self.send_command( prefix + DEVICE_COMMAND_TEMPLATES[prefix][POWER_OFF] ) if prefix in DEVICE_COMMAND_TEMPLATES: configuration = prefix + DEVICE_COMMAND_TEMPLATES[prefix]["base"] else: configuration = prefix + DEVICE_COMMAND_TEMPLATES["fallback"]["base"] configuration = configuration.replace("[po]", str(power.value)) configuration = configuration.replace("[mo]", str(operation_mode.value)) configuration = configuration.replace("[wi]", str(fan_speed.value)) configuration = configuration.replace("[sw]", str(swing_mode.value)) configuration = configuration.replace("[tt]", format(target_temperature, "X")) configuration = configuration.replace("[li]", str(led.value)) temperature = format((1 + target_temperature - 17) % 16, "X") configuration = configuration.replace("[tt1]", temperature) temperature = format((4 + target_temperature - 17) % 16, "X") configuration = configuration.replace("[tt4]", temperature) temperature = format((7 + target_temperature - 17) % 16, "X") configuration = configuration.replace("[tt7]", temperature) configuration = configuration + suffix return self.send_command(configuration)
[docs] class AirConditioningCompanionV3(AirConditioningCompanion): def __init__( self, ip: Optional[str] = None, token: Optional[str] = None, start_id: int = 0, debug: int = 0, lazy_discover: bool = True, ) -> None: super().__init__( ip, token, start_id, debug, lazy_discover, model=MODEL_ACPARTNER_V3 )
[docs] @command(default_output=format_output("Powering socket on")) def socket_on(self): """Socket power on.""" return self.send("toggle_plug", ["on"])
[docs] @command(default_output=format_output("Powering socket off")) def socket_off(self): """Socket power off.""" return self.send("toggle_plug", ["off"])
[docs] @command( default_output=format_output( "", "Power: {result.power}\n" "Power socket: {result.power_socket}\n" "Load power: {result.load_power}\n" "Air Condition model: {result.air_condition_model}\n" "LED: {result.led}\n" "Target temperature: {result.target_temperature} °C\n" "Swing mode: {result.swing_mode}\n" "Fan speed: {result.fan_speed}\n" "Mode: {result.mode}\n", ) ) def status(self) -> AirConditioningCompanionStatus: """Return device status.""" status = self.send("get_model_and_state") power_socket = self.send("get_device_prop", ["lumi.0", "plug_state"]) return AirConditioningCompanionStatus( dict(model_and_state=status, power_socket=power_socket[0]) )