From 885e7d95cf97945684214fcb4ae961071d2fcf13 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 30 Jan 2021 21:37:12 +0100 Subject: [PATCH 01/18] re-structure gateway devices --- miio/gateway.py | 1677 -------------------------- miio/gateway/__init__.py | 8 + miio/gateway/alarm.py | 85 ++ miio/gateway/devices/__init__.py | 7 + miio/gateway/devices/light.py | 43 + miio/gateway/devices/subdevice.py | 240 ++++ miio/gateway/devices/subdevices.yaml | 629 ++++++++++ miio/gateway/devices/switch.py | 40 + miio/gateway/gateway.py | 324 +++++ miio/gateway/gatewaydevice.py | 32 + miio/gateway/light.py | 162 +++ miio/gateway/radio.py | 113 ++ miio/gateway/zigbee.py | 60 + 13 files changed, 1743 insertions(+), 1677 deletions(-) delete mode 100644 miio/gateway.py create mode 100644 miio/gateway/__init__.py create mode 100644 miio/gateway/alarm.py create mode 100644 miio/gateway/devices/__init__.py create mode 100644 miio/gateway/devices/light.py create mode 100644 miio/gateway/devices/subdevice.py create mode 100644 miio/gateway/devices/subdevices.yaml create mode 100644 miio/gateway/devices/switch.py create mode 100644 miio/gateway/gateway.py create mode 100644 miio/gateway/gatewaydevice.py create mode 100644 miio/gateway/light.py create mode 100644 miio/gateway/radio.py create mode 100644 miio/gateway/zigbee.py diff --git a/miio/gateway.py b/miio/gateway.py deleted file mode 100644 index 40964e461..000000000 --- a/miio/gateway.py +++ /dev/null @@ -1,1677 +0,0 @@ -"""Xiaomi Aqara Gateway implementation using Miio protecol.""" - -import logging -from datetime import datetime -from enum import Enum, IntEnum -from typing import Optional, Tuple - -import attr -import click - -from .click_common import EnumType, command, format_output -from .device import Device -from .exceptions import DeviceError, DeviceException -from .utils import brightness_and_color_to_int, int_to_brightness, int_to_rgb - -_LOGGER = logging.getLogger(__name__) - -GATEWAY_MODEL_CHINA = "lumi.gateway.v3" -GATEWAY_MODEL_EU = "lumi.gateway.mieu01" -GATEWAY_MODEL_ZIG3 = "lumi.gateway.mgl03" -GATEWAY_MODEL_AQARA = "lumi.gateway.aqhm01" -GATEWAY_MODEL_AC_V1 = "lumi.acpartner.v1" -GATEWAY_MODEL_AC_V2 = "lumi.acpartner.v2" -GATEWAY_MODEL_AC_V3 = "lumi.acpartner.v3" - -color_map = { - "red": (255, 0, 0), - "green": (0, 255, 0), - "blue": (0, 0, 255), - "white": (255, 255, 255), - "yellow": (255, 255, 0), - "orange": (255, 165, 0), - "aqua": (0, 255, 255), - "olive": (128, 128, 0), - "purple": (128, 0, 128), -} - - -class GatewayException(DeviceException): - """Exception for the Xioami Gateway communication.""" - - -class DeviceType(IntEnum): - """DeviceType matching using the values provided by Xiaomi.""" - - Unknown = -1 - Gateway = 0 # lumi.0 - Switch = 1 # lumi.sensor_switch - Motion = 2 # lumi.sensor_motion - Magnet = 3 # lumi.sensor_magnet - SwitchTwoChannels = 7 # lumi.ctrl_neutral2 - Cube = 8 # lumi.sensor_cube.v1 - SwitchOneChannel = 9 # lumi.ctrl_neutral1.v1 - SensorHT = 10 # lumi.sensor_ht - Plug = 11 # lumi.plug - RemoteSwitchDoubleV1 = 12 # lumi.sensor_86sw2.v1 - CurtainV1 = 13 # lumi.curtain - RemoteSwitchSingleV1 = 14 # lumi.sensor_86sw1.v1 - SensorSmoke = 15 # lumi.sensor_smoke - AqaraWallOutletV1 = 17 # lumi.ctrl_86plug.v1 - SensorNatgas = 18 # lumi.sensor_natgas - AqaraHT = 19 # lumi.weather.v1 - SwitchLiveOneChannel = 20 # lumi.ctrl_ln1 - SwitchLiveTwoChannels = 21 # lumi.ctrl_ln2 - AqaraSwitch = 51 # lumi.sensor_switch.aq2 - AqaraMotion = 52 # lumi.sensor_motion.aq2 - AqaraMagnet = 53 # lumi.sensor_magnet.aq2 - AqaraRelayTwoChannels = 54 # lumi.relay.c2acn01 - AqaraWaterLeak = 55 # lumi.sensor_wleak.aq1 - AqaraVibration = 56 # lumi.vibration.aq1 - DoorLockS1 = 59 # lumi.lock.aq1 - AqaraSquareButtonV3 = 62 # lumi.sensor_switch.aq3 - AqaraSwitchOneChannel = 63 # lumi.ctrl_ln1.aq1 - AqaraSwitchTwoChannels = 64 # lumi.ctrl_ln2.aq1 - AqaraWallOutlet = 65 # lumi.ctrl_86plug.aq1 - AqaraSmartBulbE27 = 66 # lumi.light.aqcn02 - CubeV2 = 68 # lumi.sensor_cube.aqgl01 - LockS2 = 70 # lumi.lock.acn02 - Curtain = 71 # lumi.curtain.aq2 - CurtainB1 = 72 # lumi.curtain.hagl04 - LockV1 = 81 # lumi.lock.v1 - IkeaBulb82 = 82 # ikea.light.led1545g12 - IkeaBulb83 = 83 # ikea.light.led1546g12 - IkeaBulb84 = 84 # ikea.light.led1536g5 - IkeaBulb85 = 85 # ikea.light.led1537r6 - IkeaBulb86 = 86 # ikea.light.led1623g12 - IkeaBulb87 = 87 # ikea.light.led1650r5 - IkeaBulb88 = 88 # ikea.light.led1649c5 - AqaraSquareButton = 133 # lumi.remote.b1acn01 - RemoteSwitchSingle = 134 # lumi.remote.b186acn01 - RemoteSwitchDouble = 135 # lumi.remote.b286acn01 - LockS2Pro = 163 # lumi.lock.acn03 - D1RemoteSwitchSingle = 171 # lumi.remote.b186acn02 - D1RemoteSwitchDouble = 172 # lumi.remote.b286acn02 - D1WallSwitchTriple = 176 # lumi.switch.n3acn3 - D1WallSwitchTripleNN = 177 # lumi.switch.l3acn3 - ThermostatS2 = 207 # lumi.airrtc.tcpecn02 - - -# 166 - lumi.lock.acn05 -# 167 - lumi.switch.b1lacn02 -# 168 - lumi.switch.b2lacn02 -# 169 - lumi.switch.b1nacn02 -# 170 - lumi.switch.b2nacn02 -# 202 - lumi.dimmer.rgbegl01 -# 203 - lumi.dimmer.c3egl01 -# 204 - lumi.dimmer.cwegl01 -# 205 - lumi.airrtc.vrfegl01 -# 206 - lumi.airrtc.tcpecn01 - - -@attr.s(auto_attribs=True) -class SubDeviceInfo: - """SubDevice discovery info.""" - - sid: str - type_id: int - unknown: int - unknown2: int - fw_ver: int - - -class Gateway(Device): - """Main class representing the Xiaomi Gateway. - - Use the given property getters to access specific functionalities such - as `alarm` (for alarm controls) or `light` (for lights). - - Commands whose functionality or parameters are unknown, - feel free to implement! - * toggle_device - * toggle_plug - * remove_all_bind - * list_bind [0] - * bind_page - * bind - * remove_bind - - * self.get_prop("used_for_public") # Return the 'used_for_public' status, return value: [0] or [1], probably this has to do with developer mode. - * self.set_prop("used_for_public", state) # Set the 'used_for_public' state, value: 0 or 1, probably this has to do with developer mode. - - * welcome - * set_curtain_level - - * get_corridor_on_time - * set_corridor_light ["off"] - * get_corridor_light -> "on" - - * set_default_sound - * set_doorbell_push, get_doorbell_push ["off"] - * set_doorbell_volume [100], get_doorbell_volume - * set_gateway_volume, get_gateway_volume - * set_clock_volume - * set_clock - * get_sys_data - * update_neighbor_token [{"did":x, "token":x, "ip":x}] - - ## property getters - * ctrl_device_prop - * get_device_prop_exp [[sid, list, of, properties]] - - ## scene - * get_lumi_bind ["scene", ] for rooms/devices - """ - - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - ) -> None: - super().__init__(ip, token, start_id, debug, lazy_discover) - self._alarm = GatewayAlarm(parent=self) - self._radio = GatewayRadio(parent=self) - self._zigbee = GatewayZigbee(parent=self) - self._light = GatewayLight(parent=self) - self._devices = {} - self._info = None - - @property - def alarm(self) -> "GatewayAlarm": - """Return alarm control interface.""" - # example: gateway.alarm.on() - return self._alarm - - @property - def radio(self) -> "GatewayRadio": - """Return radio control interface.""" - return self._radio - - @property - def zigbee(self) -> "GatewayZigbee": - """Return zigbee control interface.""" - return self._zigbee - - @property - def light(self) -> "GatewayLight": - """Return light control interface.""" - return self._light - - @property - def devices(self): - """Return a dict of the already discovered devices.""" - return self._devices - - @property - def model(self): - """Return the zigbee model of the gateway.""" - # Check if catch already has the gateway info, otherwise get it from the device - if self._info is None: - self._info = self.info() - return self._info.model - - @command() - def discover_devices(self): - """Discovers SubDevices and returns a list of the discovered devices.""" - # from https://github.com/aholstenson/miio/issues/26 - device_type_mapping = { - DeviceType.Switch: Switch, - DeviceType.Motion: Motion, - DeviceType.Magnet: Magnet, - DeviceType.SwitchTwoChannels: SwitchTwoChannels, - DeviceType.Cube: Cube, - DeviceType.SwitchOneChannel: SwitchOneChannel, - DeviceType.SensorHT: SensorHT, - DeviceType.Plug: Plug, - DeviceType.RemoteSwitchDoubleV1: RemoteSwitchDoubleV1, - DeviceType.CurtainV1: CurtainV1, - DeviceType.RemoteSwitchSingleV1: RemoteSwitchSingleV1, - DeviceType.SensorSmoke: SensorSmoke, - DeviceType.AqaraWallOutletV1: AqaraWallOutletV1, - DeviceType.SensorNatgas: SensorNatgas, - DeviceType.AqaraHT: AqaraHT, - DeviceType.SwitchLiveOneChannel: SwitchLiveOneChannel, - DeviceType.SwitchLiveTwoChannels: SwitchLiveTwoChannels, - DeviceType.AqaraSwitch: AqaraSwitch, - DeviceType.AqaraMotion: AqaraMotion, - DeviceType.AqaraMagnet: AqaraMagnet, - DeviceType.AqaraRelayTwoChannels: AqaraRelayTwoChannels, - DeviceType.AqaraWaterLeak: AqaraWaterLeak, - DeviceType.AqaraVibration: AqaraVibration, - DeviceType.DoorLockS1: DoorLockS1, - DeviceType.AqaraSquareButtonV3: AqaraSquareButtonV3, - DeviceType.AqaraSwitchOneChannel: AqaraSwitchOneChannel, - DeviceType.AqaraSwitchTwoChannels: AqaraSwitchTwoChannels, - DeviceType.AqaraWallOutlet: AqaraWallOutlet, - DeviceType.AqaraSmartBulbE27: AqaraSmartBulbE27, - DeviceType.CubeV2: CubeV2, - DeviceType.LockS2: LockS2, - DeviceType.Curtain: Curtain, - DeviceType.CurtainB1: CurtainB1, - DeviceType.LockV1: LockV1, - DeviceType.IkeaBulb82: IkeaBulb82, - DeviceType.IkeaBulb83: IkeaBulb83, - DeviceType.IkeaBulb84: IkeaBulb84, - DeviceType.IkeaBulb85: IkeaBulb85, - DeviceType.IkeaBulb86: IkeaBulb86, - DeviceType.IkeaBulb87: IkeaBulb87, - DeviceType.IkeaBulb88: IkeaBulb88, - DeviceType.AqaraSquareButton: AqaraSquareButton, - DeviceType.RemoteSwitchSingle: RemoteSwitchSingle, - DeviceType.RemoteSwitchDouble: RemoteSwitchDouble, - DeviceType.LockS2Pro: LockS2Pro, - DeviceType.D1RemoteSwitchSingle: D1RemoteSwitchSingle, - DeviceType.D1RemoteSwitchDouble: D1RemoteSwitchDouble, - DeviceType.D1WallSwitchTriple: D1WallSwitchTriple, - DeviceType.D1WallSwitchTripleNN: D1WallSwitchTripleNN, - DeviceType.ThermostatS2: ThermostatS2, - } - self._devices = {} - - # Skip the models which do not support getting the device list - if self.model == GATEWAY_MODEL_EU: - _LOGGER.warning( - "Gateway model '%s' does not (yet) support getting the device list", - self.model, - ) - return self._devices - - devices_raw = self.get_prop("device_list") - - for x in range(0, len(devices_raw), 5): - # Extract discovered information - dev_info = SubDeviceInfo(*devices_raw[x : x + 5]) - - # Construct DeviceType - try: - device_type = DeviceType(dev_info.type_id) - except ValueError: - _LOGGER.warning( - "Unknown subdevice type %s discovered, " - "of Xiaomi gateway with ip: %s", - dev_info, - self.ip, - ) - device_type = DeviceType(-1) - - # Obtain the correct subdevice class, ignoring the gateway itself - subdevice_cls = device_type_mapping.get(device_type) - if subdevice_cls is None and device_type != DeviceType.Gateway: - subdevice_cls = SubDevice - _LOGGER.info( - "Gateway device type '%s' " - "does not have device specific methods defined, " - "only basic default methods will be available", - device_type.name, - ) - - # Initialize and save the subdevice, ignoring the gateway itself - if device_type != DeviceType.Gateway: - self._devices[dev_info.sid] = subdevice_cls(self, dev_info) - if self._devices[dev_info.sid].status == {}: - _LOGGER.info( - "Discovered subdevice type '%s', has no device specific properties defined, " - "this device has not been fully implemented yet (model: %s, name: %s).", - device_type.name, - self._devices[dev_info.sid].model, - self._devices[dev_info.sid].name, - ) - - return self._devices - - @command(click.argument("property")) - def get_prop(self, property): - """Get the value of a property for given sid.""" - return self.send("get_device_prop", ["lumi.0", property]) - - @command(click.argument("properties", nargs=-1)) - def get_prop_exp(self, properties): - """Get the value of a bunch of properties for given sid.""" - return self.send("get_device_prop_exp", [["lumi.0"] + list(properties)]) - - @command(click.argument("property"), click.argument("value")) - def set_prop(self, property, value): - """Set the device property.""" - return self.send("set_device_prop", {"sid": "lumi.0", property: value}) - - @command() - def clock(self): - """Alarm clock.""" - # payload of clock volume ("get_clock_volume") - # already in get_clock response - return self.send("get_clock") - - # Developer key - @command() - def get_developer_key(self): - """Return the developer API key.""" - return self.send("get_lumi_dpf_aes_key")[0] - - @command(click.argument("key")) - def set_developer_key(self, key): - """Set the developer API key.""" - if len(key) != 16: - click.echo("Key must be of length 16, was %s" % len(key)) - - return self.send("set_lumi_dpf_aes_key", [key]) - - @command() - def enable_telnet(self): - """Enable root telnet acces to the operating system, use login "admin" or "app", - no password.""" - try: - return self.send("enable_telnet_service") - except DeviceError: - _LOGGER.error( - "Gateway model '%s' does not (yet) support enabling the telnet interface", - self.model, - ) - return None - - @command() - def timezone(self): - """Get current timezone.""" - return self.get_prop("tzone_sec") - - @command() - def get_illumination(self): - """Get illumination. - - In lux? - """ - try: - return self.send("get_illumination").pop() - except Exception as ex: - raise GatewayException( - "Got an exception while getting gateway illumination" - ) from ex - - -class GatewayDevice(Device): - """GatewayDevice class Specifies the init method for all gateway device - functionalities.""" - - def __init__( - self, - ip: str = None, - token: str = None, - start_id: int = 0, - debug: int = 0, - lazy_discover: bool = True, - parent: Gateway = None, - ) -> None: - if parent is not None: - self._gateway = parent - else: - self._gateway = Device(ip, token, start_id, debug, lazy_discover) - _LOGGER.debug( - "Creating new device instance, only use this for cli interface" - ) - - -class GatewayAlarm(GatewayDevice): - """Class representing the Xiaomi Gateway Alarm.""" - - @command(default_output=format_output("[alarm_status]")) - def status(self) -> str: - """Return the alarm status from the device.""" - # Response: 'on', 'off', 'oning' - return self._gateway.send("get_arming").pop() - - @command(default_output=format_output("Turning alarm on")) - def on(self): - """Turn alarm on.""" - return self._gateway.send("set_arming", ["on"]) - - @command(default_output=format_output("Turning alarm off")) - def off(self): - """Turn alarm off.""" - return self._gateway.send("set_arming", ["off"]) - - @command() - def arming_time(self) -> int: - """Return time in seconds the alarm stays 'oning' before transitioning to - 'on'.""" - # Response: 5, 15, 30, 60 - return self._gateway.send("get_arm_wait_time").pop() - - @command(click.argument("seconds")) - def set_arming_time(self, seconds): - """Set time the alarm stays at 'oning' before transitioning to 'on'.""" - return self._gateway.send("set_arm_wait_time", [seconds]) - - @command() - def triggering_time(self) -> int: - """Return the time in seconds the alarm is going off when triggered.""" - # Response: 30, 60, etc. - return self._gateway.get_prop("alarm_time_len").pop() - - @command(click.argument("seconds")) - def set_triggering_time(self, seconds): - """Set the time in seconds the alarm is going off when triggered.""" - return self._gateway.set_prop("alarm_time_len", seconds) - - @command() - def triggering_light(self) -> int: - """Return the time the gateway light blinks when the alarm is triggerd.""" - # Response: 0=do not blink, 1=always blink, x>1=blink for x seconds - return self._gateway.get_prop("en_alarm_light").pop() - - @command(click.argument("seconds")) - def set_triggering_light(self, seconds): - """Set the time the gateway light blinks when the alarm is triggerd.""" - # values: 0=do not blink, 1=always blink, x>1=blink for x seconds - return self._gateway.set_prop("en_alarm_light", seconds) - - @command() - def triggering_volume(self) -> int: - """Return the volume level at which alarms go off [0-100].""" - return self._gateway.send("get_alarming_volume").pop() - - @command(click.argument("volume")) - def set_triggering_volume(self, volume): - """Set the volume level at which alarms go off [0-100].""" - return self._gateway.send("set_alarming_volume", [volume]) - - @command() - def last_status_change_time(self) -> datetime: - """Return the last time the alarm changed status.""" - return datetime.fromtimestamp(self._gateway.send("get_arming_time").pop()) - - -class GatewayZigbee(GatewayDevice): - """Zigbee controls.""" - - @command() - def get_zigbee_version(self): - """timeouts on device.""" - return self._gateway.send("get_zigbee_device_version") - - @command() - def get_zigbee_channel(self): - """Return currently used zigbee channel.""" - return self._gateway.send("get_zigbee_channel")[0] - - @command(click.argument("channel")) - def set_zigbee_channel(self, channel): - """Set zigbee channel.""" - return self._gateway.send("set_zigbee_channel", [channel]) - - @command(click.argument("timeout", type=int)) - def zigbee_pair(self, timeout): - """Start pairing, use 0 to disable.""" - return self._gateway.send("start_zigbee_join", [timeout]) - - def send_to_zigbee(self): - """How does this differ from writing? - - Unknown. - """ - raise NotImplementedError() - return self._gateway.send("send_to_zigbee") - - def read_zigbee_eep(self): - """Read eeprom?""" - raise NotImplementedError() - return self._gateway.send("read_zig_eep", [0]) # 'ok' - - def read_zigbee_attribute(self): - """Read zigbee data?""" - raise NotImplementedError() - return self._gateway.send("read_zigbee_attribute", [0x0000, 0x0080]) - - def write_zigbee_attribute(self): - """Unknown parameters.""" - raise NotImplementedError() - return self._gateway.send("write_zigbee_attribute") - - @command() - def zigbee_unpair_all(self): - """Unpair all devices.""" - return self._gateway.send("remove_all_device") - - def zigbee_unpair(self, sid): - """Unpair a device.""" - # get a device obj an call dev.unpair() - raise NotImplementedError() - - -class GatewayRadio(GatewayDevice): - """Radio controls for the gateway.""" - - @command() - def get_radio_info(self): - """Radio play info.""" - return self._gateway.send("get_prop_fm") - - @command(click.argument("volume")) - def set_radio_volume(self, volume): - """Set radio volume.""" - return self._gateway.send("set_fm_volume", [volume]) - - def play_music_new(self): - """Unknown.""" - # {'from': '4', 'id': 9514, - # 'method': 'set_default_music', 'params': [2, '21']} - # {'from': '4', 'id': 9515, - # 'method': 'play_music_new', 'params': ['21', 0]} - raise NotImplementedError() - - def play_specify_fm(self): - """play specific stream?""" - raise NotImplementedError() - # {"from": "4", "id": 65055, "method": "play_specify_fm", - # "params": {"id": 764, "type": 0, - # "url": "http://live.xmcdn.com/live/764/64.m3u8"}} - return self._gateway.send("play_specify_fm") - - def play_fm(self): - """radio on/off?""" - raise NotImplementedError() - # play_fm","params":["off"]} - return self._gateway.send("play_fm") - - def volume_ctrl_fm(self): - """Unknown.""" - raise NotImplementedError() - return self._gateway.send("volume_ctrl_fm") - - def get_channels(self): - """Unknown.""" - raise NotImplementedError() - # "method": "get_channels", "params": {"start": 0}} - return self._gateway.send("get_channels") - - def add_channels(self): - """Unknown.""" - raise NotImplementedError() - return self._gateway.send("add_channels") - - def remove_channels(self): - """Unknown.""" - raise NotImplementedError() - return self._gateway.send("remove_channels") - - def get_default_music(self): - """seems to timeout (w/o internet).""" - # params [0,1,2] - raise NotImplementedError() - return self._gateway.send("get_default_music") - - @command() - def get_music_info(self): - """Unknown.""" - info = self._gateway.send("get_music_info") - click.echo("info: %s" % info) - free_space = self._gateway.send("get_music_free_space") - click.echo("free space: %s" % free_space) - - @command() - def get_mute(self): - """mute of what?""" - return self._gateway.send("get_mute") - - def download_music(self): - """Unknown.""" - raise NotImplementedError() - return self._gateway.send("download_music") - - def delete_music(self): - """delete music.""" - raise NotImplementedError() - return self._gateway.send("delete_music") - - def download_user_music(self): - """Unknown.""" - raise NotImplementedError() - return self._gateway.send("download_user_music") - - def get_download_progress(self): - """progress for music downloads or updates?""" - # returns [':0'] - raise NotImplementedError() - return self._gateway.send("get_download_progress") - - @command() - def set_sound_playing(self): - """stop playing?""" - return self._gateway.send("set_sound_playing", ["off"]) - - def set_default_music(self): - """Unknown.""" - raise NotImplementedError() - # method":"set_default_music","params":[0,"2"]} - - -class GatewayLight(GatewayDevice): - """Light controls for the gateway. - - The gateway LEDs can be controlled using 'rgb' or 'night_light' methods. The - 'night_light' methods control the same light as the 'rgb' methods, but has a - separate memory for brightness and color. Changing the 'rgb' light does not affect - the stored state of the 'night_light', while changing the 'night_light' does effect - the state of the 'rgb' light. - """ - - @command() - def rgb_status(self): - """Get current status of the light. Always represents the current status of the - light as opposed to 'night_light_status'. - - Example: - {"is_on": false, "brightness": 0, "rgb": (0, 0, 0)} - """ - # Returns {"is_on": false, "brightness": 0, "rgb": (0, 0, 0)} when light is off - state_int = self._gateway.send("get_rgb").pop() - brightness = int_to_brightness(state_int) - rgb = int_to_rgb(state_int) - is_on = brightness > 0 - - return {"is_on": is_on, "brightness": brightness, "rgb": rgb} - - @command() - def night_light_status(self): - """Get status of the night light. This command only gives the correct status of - the LEDs if the last command was a 'night_light' command and not a 'rgb' light - command, otherwise it gives the stored values of the 'night_light'. - - Example: - {"is_on": false, "brightness": 0, "rgb": (0, 0, 0)} - """ - state_int = self._gateway.send("get_night_light_rgb").pop() - brightness = int_to_brightness(state_int) - rgb = int_to_rgb(state_int) - is_on = brightness > 0 - - return {"is_on": is_on, "brightness": brightness, "rgb": rgb} - - @command( - click.argument("brightness", type=int), - click.argument("rgb", type=(int, int, int)), - ) - def set_rgb(self, brightness: int, rgb: Tuple[int, int, int]): - """Set gateway light using brightness and rgb tuple.""" - brightness_and_color = brightness_and_color_to_int(brightness, rgb) - - return self._gateway.send("set_rgb", [brightness_and_color]) - - @command( - click.argument("brightness", type=int), - click.argument("rgb", type=(int, int, int)), - ) - def set_night_light(self, brightness: int, rgb: Tuple[int, int, int]): - """Set gateway night light using brightness and rgb tuple.""" - brightness_and_color = brightness_and_color_to_int(brightness, rgb) - - return self._gateway.send("set_night_light_rgb", [brightness_and_color]) - - @command(click.argument("brightness", type=int)) - def set_rgb_brightness(self, brightness: int): - """Set gateway light brightness (0-100).""" - if 100 < brightness < 0: - raise Exception("Brightness must be between 0 and 100") - current_color = self.rgb_status()["rgb"] - - return self.set_rgb(brightness, current_color) - - @command(click.argument("brightness", type=int)) - def set_night_light_brightness(self, brightness: int): - """Set night light brightness (0-100).""" - if 100 < brightness < 0: - raise Exception("Brightness must be between 0 and 100") - current_color = self.night_light_status()["rgb"] - - return self.set_night_light(brightness, current_color) - - @command(click.argument("color_name", type=str)) - def set_rgb_color(self, color_name: str): - """Set gateway light color using color name ('color_map' variable in the source - holds the valid values).""" - if color_name not in color_map.keys(): - raise Exception( - "Cannot find {color} in {colors}".format( - color=color_name, colors=color_map.keys() - ) - ) - current_brightness = self.rgb_status()["brightness"] - - return self.set_rgb(current_brightness, color_map[color_name]) - - @command(click.argument("color_name", type=str)) - def set_night_light_color(self, color_name: str): - """Set night light color using color name ('color_map' variable in the source - holds the valid values).""" - if color_name not in color_map.keys(): - raise Exception( - "Cannot find {color} in {colors}".format( - color=color_name, colors=color_map.keys() - ) - ) - current_brightness = self.night_light_status()["brightness"] - - return self.set_night_light(current_brightness, color_map[color_name]) - - @command( - click.argument("color_name", type=str), - click.argument("brightness", type=int), - ) - def set_rgb_using_name(self, color_name: str, brightness: int): - """Set gateway light color (using color name, 'color_map' variable in the source - holds the valid values) and brightness (0-100).""" - if 100 < brightness < 0: - raise Exception("Brightness must be between 0 and 100") - if color_name not in color_map.keys(): - raise Exception( - "Cannot find {color} in {colors}".format( - color=color_name, colors=color_map.keys() - ) - ) - - return self.set_rgb(brightness, color_map[color_name]) - - @command( - click.argument("color_name", type=str), - click.argument("brightness", type=int), - ) - def set_night_light_using_name(self, color_name: str, brightness: int): - """Set night light color (using color name, 'color_map' variable in the source - holds the valid values) and brightness (0-100).""" - if 100 < brightness < 0: - raise Exception("Brightness must be between 0 and 100") - if color_name not in color_map.keys(): - raise Exception( - "Cannot find {color} in {colors}".format( - color=color_name, colors=color_map.keys() - ) - ) - - return self.set_night_light(brightness, color_map[color_name]) - - -class SubDevice: - """Base class for all subdevices of the gateway these devices are connected through - zigbee.""" - - _zigbee_model = "unknown" - _model = "unknown" - _name = "unknown" - - @attr.s(auto_attribs=True) - class props: - """Defines properties of the specific device.""" - - def __init__( - self, - gw: Gateway = None, - dev_info: SubDeviceInfo = None, - ) -> None: - self._gw = gw - self.sid = dev_info.sid - self._battery = None - self._voltage = None - self._fw_ver = dev_info.fw_ver - self._props = self.props() - try: - self.type = DeviceType(dev_info.type_id) - except ValueError: - self.type = DeviceType.Unknown - - def __repr__(self): - return "" % ( - self.device_type, - self.sid, - self.model, - self.zigbee_model, - self.firmware_version, - self.get_battery(), - self.get_voltage(), - self.status, - ) - - @property - def status(self): - """Return sub-device status as a dict containing all properties.""" - return attr.asdict(self._props) - - @property - def device_type(self): - """Return the device type name.""" - return self.type.name - - @property - def name(self): - """Return the name of the device.""" - return f"{self._name} ({self.sid})" - - @property - def model(self): - """Return the device model.""" - return self._model - - @property - def zigbee_model(self): - """Return the zigbee device model.""" - return self._zigbee_model - - @property - def firmware_version(self): - """Return the firmware version.""" - return self._fw_ver - - @property - def battery(self): - """Return the battery level in %.""" - return self._battery - - @property - def voltage(self): - """Return the battery voltage in V.""" - return self._voltage - - @command() - def update(self): - """Update the device-specific properties.""" - _LOGGER.debug( - "Subdevice '%s' does not have a device specific update method defined", - self.device_type, - ) - - @command() - def send(self, command): - """Send a command/query to the subdevice.""" - try: - return self._gw.send(command, [self.sid]) - except Exception as ex: - raise GatewayException( - "Got an exception while sending command %s" % (command) - ) from ex - - @command() - def send_arg(self, command, arguments): - """Send a command/query including arguments to the subdevice.""" - try: - return self._gw.send(command, arguments, extra_parameters={"sid": self.sid}) - except Exception as ex: - raise GatewayException( - "Got an exception while sending " - "command '%s' with arguments '%s'" % (command, str(arguments)) - ) from ex - - @command(click.argument("property")) - def get_property(self, property): - """Get the value of a property of the subdevice.""" - try: - response = self._gw.send("get_device_prop", [self.sid, property]) - except Exception as ex: - raise GatewayException( - "Got an exception while fetching property %s" % (property) - ) from ex - - if not response: - raise GatewayException( - "Empty response while fetching property '%s': %s" % (property, response) - ) - - return response - - @command(click.argument("properties", nargs=-1)) - def get_property_exp(self, properties): - """Get the value of a bunch of properties of the subdevice.""" - try: - response = self._gw.send( - "get_device_prop_exp", [[self.sid] + list(properties)] - ).pop() - except Exception as ex: - raise GatewayException( - "Got an exception while fetching properties %s: %s" % (properties) - ) from ex - - if len(list(properties)) != len(response): - raise GatewayException( - "unexpected result while fetching properties %s: %s" - % (properties, response) - ) - - return response - - @command(click.argument("property"), click.argument("value")) - def set_property(self, property, value): - """Set a device property of the subdevice.""" - try: - return self._gw.send("set_device_prop", {"sid": self.sid, property: value}) - except Exception as ex: - raise GatewayException( - "Got an exception while setting propertie %s to value %s" - % (property, str(value)) - ) from ex - - @command() - def unpair(self): - """Unpair this device from the gateway.""" - return self.send("remove_device") - - @command() - def get_battery(self): - """Update the battery level, if available.""" - if self._gw.model != GATEWAY_MODEL_EU: - self._battery = self.send("get_battery").pop() - else: - _LOGGER.info( - "Gateway model '%s' does not (yet) support get_battery", - self._gw.model, - ) - return self._battery - - @command() - def get_voltage(self): - """Update the battery voltage, if available.""" - if self._gw.model == GATEWAY_MODEL_EU: - self._voltage = self.get_property("voltage").pop() / 1000 - else: - _LOGGER.info( - "Gateway model '%s' does not (yet) support get_voltage", - self._gw.model, - ) - return self._voltage - - @command() - def get_firmware_version(self) -> Optional[int]: - """Returns firmware version.""" - try: - self._fw_ver = self.get_property("fw_ver").pop() - except Exception as ex: - _LOGGER.info( - "get_firmware_version failed, returning firmware version from discovery info: %s", - ex, - ) - return self._fw_ver - - -class Switch(SubDevice): - """Subdevice Switch specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_switch" - _model = "WXKG01LM" - _name = "Button" - - -class Motion(SubDevice): - """Subdevice Motion specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_motion" - _model = "RTCGQ01LM" - _name = "Motion sensor" - - -class Magnet(SubDevice): - """Subdevice Magnet specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_magnet" - _model = "MCCGQ01LM" - _name = "Door sensor" - - -class SwitchTwoChannels(SubDevice): - """Subdevice SwitchTwoChannels specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.ctrl_neutral2" - _model = "QBKG03LM" - _name = "Wall switch double no neutral" - - -class Cube(SubDevice): - """Subdevice Cube specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_cube.v1" - _model = "MFKZQ01LM" - _name = "Cube" - - -class SwitchOneChannel(SubDevice): - """Subdevice SwitchOneChannel specific properties and methods.""" - - properties = ["neutral_0"] - _zigbee_model = "lumi.ctrl_neutral1.v1" - _model = "QBKG04LM" - _name = "Wall switch no neutral" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - status: str = None # 'on' / 'off' - - @command() - def update(self): - """Update all device properties.""" - values = self.get_property_exp(self.properties) - self._props.status = values[0] - - @command() - def toggle(self): - """Toggle Switch One Channel.""" - return self.send_arg("toggle_ctrl_neutral", ["channel_0", "toggle"]).pop() - - @command() - def on(self): - """Turn on Switch One Channel.""" - return self.send_arg("toggle_ctrl_neutral", ["channel_0", "on"]).pop() - - @command() - def off(self): - """Turn off Switch One Channel.""" - return self.send_arg("toggle_ctrl_neutral", ["channel_0", "off"]).pop() - - -class SensorHT(SubDevice): - """Subdevice SensorHT specific properties and methods.""" - - accessor = "get_prop_sensor_ht" - properties = ["temperature", "humidity"] - _zigbee_model = "lumi.sensor_ht" - _model = "WSDCGQ01LM" - _name = "Weather sensor" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - temperature: int = None # in degrees celsius - humidity: int = None # in % - - @command() - def update(self): - """Update all device properties.""" - values = self.get_property_exp(self.properties) - try: - self._props.temperature = values[0] / 100 - self._props.humidity = values[1] / 100 - except Exception as ex: - raise GatewayException( - "One or more unexpected results while " - "fetching properties %s: %s" % (self.properties, values) - ) from ex - - -class Plug(SubDevice): - """Subdevice Plug specific properties and methods.""" - - accessor = "get_prop_plug" - properties = ["neutral_0", "load_power"] - _zigbee_model = "lumi.plug" - _model = "ZNCZ02LM" - _name = "Plug" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - status: str = None # 'on' / 'off' - load_power: int = None # power consumption in Watt - - @command() - def update(self): - """Update all device properties.""" - values = self.get_property_exp(self.properties) - self._props.status = values[0] - self._props.load_power = values[1] - - @command() - def toggle(self): - """Toggle Plug.""" - return self.send_arg("toggle_plug", ["channel_0", "toggle"]).pop() - - @command() - def on(self): - """Turn on Plug.""" - return self.send_arg("toggle_plug", ["channel_0", "on"]).pop() - - @command() - def off(self): - """Turn off Plug.""" - return self.send_arg("toggle_plug", ["channel_0", "off"]).pop() - - -class RemoteSwitchDoubleV1(SubDevice): - """Subdevice RemoteSwitchDoubleV1 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_86sw2.v1" - _model = "WXKG02LM 2016" - _name = "Remote switch double" - - -class CurtainV1(SubDevice): - """Subdevice CurtainV1 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.curtain" - _model = "ZNCLDJ11LM" - _name = "Curtain" - - -class RemoteSwitchSingleV1(SubDevice): - """Subdevice RemoteSwitchSingleV1 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_86sw1.v1" - _model = "WXKG03LM 2016" - _name = "Remote switch single" - - -class SensorSmoke(SubDevice): - """Subdevice SensorSmoke specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_smoke" - _model = "JTYJ-GD-01LM/BW" - _name = "Honeywell smoke detector" - - -class AqaraWallOutletV1(SubDevice): - """Subdevice AqaraWallOutletV1 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.ctrl_86plug.v1" - _model = "QBCZ11LM" - _name = "Wall outlet" - - -class SensorNatgas(SubDevice): - """Subdevice SensorNatgas specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_natgas" - _model = "JTQJ-BF-01LM/BW" - _name = "Honeywell natural gas detector" - - -class AqaraHT(SubDevice): - """Subdevice AqaraHT specific properties and methods.""" - - accessor = "get_prop_sensor_ht" - properties = ["temperature", "humidity", "pressure"] - _zigbee_model = "lumi.weather.v1" - _model = "WSDCGQ11LM" - _name = "Weather sensor" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - temperature: int = None # in degrees celsius - humidity: int = None # in % - pressure: int = None # in hPa - - @command() - def update(self): - """Update all device properties.""" - values = self.get_property_exp(self.properties) - try: - self._props.temperature = values[0] / 100 - self._props.humidity = values[1] / 100 - self._props.pressure = values[2] / 100 - except Exception as ex: - raise GatewayException( - "One or more unexpected results while " - "fetching properties %s: %s" % (self.properties, values) - ) from ex - - -class SwitchLiveOneChannel(SubDevice): - """Subdevice SwitchLiveOneChannel specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.ctrl_ln1" - _model = "QBKG11LM" - _name = "Wall switch single" - - -class SwitchLiveTwoChannels(SubDevice): - """Subdevice SwitchLiveTwoChannels specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.ctrl_ln2" - _model = "QBKG12LM" - _name = "Wall switch double" - - -class AqaraSwitch(SubDevice): - """Subdevice AqaraSwitch specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_switch.aq2" - _model = "WXKG11LM 2015" - _name = "Button" - - -class AqaraMotion(SubDevice): - """Subdevice AqaraMotion specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_motion.aq2" - _model = "RTCGQ11LM" - _name = "Motion sensor" - - -class AqaraMagnet(SubDevice): - """Subdevice AqaraMagnet specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_magnet.aq2" - _model = "MCCGQ11LM" - _name = "Door sensor" - - -class AqaraRelayTwoChannels(SubDevice): - """Subdevice AqaraRelayTwoChannels specific properties and methods.""" - - properties = ["load_power", "channel_0", "channel_1"] - _zigbee_model = "lumi.relay.c2acn01" - _model = "LLKZMK11LM" - _name = "Relay" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - status_ch0: str = None # 'on' / 'off' - status_ch1: str = None # 'on' / 'off' - load_power: int = None # power consumption in ?unit? - - class AqaraRelayToggleValue(Enum): - """Options to control the relay.""" - - toggle = "toggle" - on = "on" - off = "off" - - class AqaraRelayChannel(Enum): - """Options to select wich relay to control.""" - - first = "channel_0" - second = "channel_1" - - @command() - def update(self): - """Update all device properties.""" - values = self.get_property_exp(self.properties) - self._props.load_power = values[0] - self._props.status_ch0 = values[1] - self._props.status_ch1 = values[2] - - @command( - click.argument("channel", type=EnumType(AqaraRelayChannel)), - click.argument("value", type=EnumType(AqaraRelayToggleValue)), - ) - def toggle(self, channel, value): - """Toggle Aqara Wireless Relay 2ch.""" - return self.send_arg("toggle_ctrl_neutral", [channel.value, value.value]).pop() - - -class AqaraWaterLeak(SubDevice): - """Subdevice AqaraWaterLeak specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_wleak.aq1" - _model = "SJCGQ11LM" - _name = "Water leak sensor" - - -class AqaraVibration(SubDevice): - """Subdevice AqaraVibration specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.vibration.aq1" - _model = "DJT11LM" - _name = "Vibration sensor" - - -class DoorLockS1(SubDevice): - """Subdevice DoorLockS1 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.lock.aq1" - _model = "ZNMS11LM" - _name = "Door lock S1" - - -class AqaraSquareButtonV3(SubDevice): - """Subdevice AqaraSquareButtonV3 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_switch.aq3" - _model = "WXKG12LM" - _name = "Button" - - -class AqaraSwitchOneChannel(SubDevice): - """Subdevice AqaraSwitchOneChannel specific properties and methods.""" - - properties = ["neutral_0", "load_power"] - _zigbee_model = "lumi.ctrl_ln1.aq1" - _model = "QBKG11LM" - _name = "Wall switch single" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - status: str = None # 'on' / 'off' - load_power: int = None # power consumption in ?unit? - - @command() - def update(self): - """Update all device properties.""" - values = self.get_property_exp(self.properties) - self._props.status = values[0] - self._props.load_power = values[1] - - -class AqaraSwitchTwoChannels(SubDevice): - """Subdevice AqaraSwitchTwoChannels specific properties and methods.""" - - properties = ["neutral_0", "neutral_1", "load_power"] - _zigbee_model = "lumi.ctrl_ln2.aq1" - _model = "QBKG12LM" - _name = "Wall switch double" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - status_ch0: str = None # 'on' / 'off' - status_ch1: str = None # 'on' / 'off' - load_power: int = None # power consumption in ?unit? - - @command() - def update(self): - """Update all device properties.""" - values = self.get_property_exp(self.properties) - self._props.status_ch0 = values[0] - self._props.status_ch1 = values[1] - self._props.load_power = values[2] - - -class AqaraWallOutlet(SubDevice): - """Subdevice AqaraWallOutlet specific properties and methods.""" - - properties = ["channel_0", "load_power"] - _zigbee_model = "lumi.ctrl_86plug.aq1" - _model = "QBCZ11LM" - _name = "Wall outlet" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - status: str = None # 'on' / 'off' - load_power: int = None # power consumption in Watt - - @command() - def update(self): - """Update all device properties.""" - values = self.get_property_exp(self.properties) - self._props.status = values[0] - self._props.load_power = values[1] - - @command() - def toggle(self): - """Toggle Aqara Wall Outlet.""" - return self.send_arg("toggle_plug", ["channel_0", "toggle"]).pop() - - @command() - def on(self): - """Turn on Aqara Wall Outlet.""" - return self.send_arg("toggle_plug", ["channel_0", "on"]).pop() - - @command() - def off(self): - """Turn off Aqara Wall Outlet.""" - return self.send_arg("toggle_plug", ["channel_0", "off"]).pop() - - -class AqaraSmartBulbE27(SubDevice): - """Subdevice AqaraSmartBulbE27 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.light.aqcn02" - _model = "ZNLDP12LM" - _name = "Smart bulb E27" - - @attr.s(auto_attribs=True) - class props: - """Device specific properties.""" - - status: str = None # 'on' / 'off' - brightness: int = None # in % - color_temp: int = None # cct value from _ctt_min to _ctt_max - cct_min: int = 153 - cct_max: int = 500 - - @command() - def update(self): - """Update all device properties.""" - self._props.brightness = self.send("get_bright").pop() - self._props.color_temp = self.send("get_ct").pop() - if self._props.brightness > 0 and self._props.brightness <= 100: - self._props.status = "on" - else: - self._props.status = "off" - - @command() - def on(self): - """Turn bulb on.""" - return self.send_arg("set_power", ["on"]).pop() - - @command() - def off(self): - """Turn bulb off.""" - return self.send_arg("set_power", ["off"]).pop() - - @command(click.argument("ctt", type=int)) - def set_color_temp(self, ctt): - """Set the color temperature of the bulb ctt_min-ctt_max.""" - return self.send_arg("set_ct", [ctt]).pop() - - @command(click.argument("brightness", type=int)) - def set_brightness(self, brightness): - """Set the brightness of the bulb 1-100.""" - return self.send_arg("set_bright", [brightness]).pop() - - -class CubeV2(SubDevice): - """Subdevice CubeV2 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.sensor_cube.aqgl01" - _model = "MFKZQ01LM" - _name = "Cube" - - -class LockS2(SubDevice): - """Subdevice LockS2 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.lock.acn02" - _model = "ZNMS12LM" - _name = "Door lock S2" - - -class Curtain(SubDevice): - """Subdevice Curtain specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.curtain.aq2" - _model = "ZNGZDJ11LM" - _name = "Curtain" - - -class CurtainB1(SubDevice): - """Subdevice CurtainB1 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.curtain.hagl04" - _model = "ZNCLDJ12LM" - _name = "Curtain B1" - - -class LockV1(SubDevice): - """Subdevice LockV1 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.lock.v1" - _model = "A6121" - _name = "Vima cylinder lock" - - -class IkeaBulb82(SubDevice): - """Subdevice IkeaBulb82 specific properties and methods.""" - - properties = [] - _zigbee_model = "ikea.light.led1545g12" - _model = "LED1545G12" - _name = "Ikea smart bulb E27 white" - - -class IkeaBulb83(SubDevice): - """Subdevice IkeaBulb83 specific properties and methods.""" - - properties = [] - _zigbee_model = "ikea.light.led1546g12" - _model = "LED1546G12" - _name = "Ikea smart bulb E27 white" - - -class IkeaBulb84(SubDevice): - """Subdevice IkeaBulb84 specific properties and methods.""" - - properties = [] - _zigbee_model = "ikea.light.led1536g5" - _model = "LED1536G5" - _name = "Ikea smart bulb E12 white" - - -class IkeaBulb85(SubDevice): - """Subdevice IkeaBulb85 specific properties and methods.""" - - properties = [] - _zigbee_model = "ikea.light.led1537r6" - _model = "LED1537R6" - _name = "Ikea smart bulb GU10 white" - - -class IkeaBulb86(SubDevice): - """Subdevice IkeaBulb86 specific properties and methods.""" - - properties = [] - _zigbee_model = "ikea.light.led1623g12" - _model = "LED1623G12" - _name = "Ikea smart bulb E27 white" - - -class IkeaBulb87(SubDevice): - """Subdevice IkeaBulb87 specific properties and methods.""" - - properties = [] - _zigbee_model = "ikea.light.led1650r5" - _model = "LED1650R5" - _name = "Ikea smart bulb GU10 white" - - -class IkeaBulb88(SubDevice): - """Subdevice IkeaBulb88 specific properties and methods.""" - - properties = [] - _zigbee_model = "ikea.light.led1649c5" - _model = "LED1649C5" - _name = "Ikea smart bulb E12 white" - - -class AqaraSquareButton(SubDevice): - """Subdevice AqaraSquareButton specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.remote.b1acn01" - _model = "WXKG11LM 2018" - _name = "Button" - - -class RemoteSwitchSingle(SubDevice): - """Subdevice RemoteSwitchSingle specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.remote.b186acn01" - _model = "WXKG03LM 2018" - _name = "Remote switch single" - - -class RemoteSwitchDouble(SubDevice): - """Subdevice RemoteSwitchDouble specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.remote.b286acn01" - _model = "WXKG02LM 2018" - _name = "Remote switch double" - - -class LockS2Pro(SubDevice): - """Subdevice LockS2Pro specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.lock.acn03" - _model = "ZNMS13LM" - _name = "Door lock S2 pro" - - -class D1RemoteSwitchSingle(SubDevice): - """Subdevice D1RemoteSwitchSingle specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.remote.b186acn02" - _model = "WXKG06LM" - _name = "D1 remote switch single" - - -class D1RemoteSwitchDouble(SubDevice): - """Subdevice D1RemoteSwitchDouble specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.remote.b286acn02" - _model = "WXKG07LM" - _name = "D1 remote switch double" - - -class D1WallSwitchTriple(SubDevice): - """Subdevice D1WallSwitchTriple specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.switch.n3acn3" - _model = "QBKG26LM" - _name = "D1 wall switch triple" - - -class D1WallSwitchTripleNN(SubDevice): - """Subdevice D1WallSwitchTripleNN specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.switch.l3acn3" - _model = "QBKG25LM" - _name = "D1 wall switch triple no neutral" - - -class ThermostatS2(SubDevice): - """Subdevice ThermostatS2 specific properties and methods.""" - - properties = [] - _zigbee_model = "lumi.airrtc.tcpecn02" - _model = "KTWKQ03ES" - _name = "Thermostat S2" diff --git a/miio/gateway/__init__.py b/miio/gateway/__init__.py new file mode 100644 index 000000000..c162fed80 --- /dev/null +++ b/miio/gateway/__init__.py @@ -0,0 +1,8 @@ +"""Xiaomi Gateway implementation using Miio protecol.""" + +# flake8: noqa +from .alarm import Alarm +from .gateway import Gateway +from .light import Light +from .radio import Radio +from .zigbee import Zigbee diff --git a/miio/gateway/alarm.py b/miio/gateway/alarm.py new file mode 100644 index 000000000..9b56a97a1 --- /dev/null +++ b/miio/gateway/alarm.py @@ -0,0 +1,85 @@ +"""Xiaomi Gateway Alarm implementation.""" + +from datetime import datetime + +import click + +from ..click_common import command, format_output +from .gatewaydevice import GatewayDevice + + +class Alarm(GatewayDevice): + """Class representing the Xiaomi Gateway Alarm.""" + + @command(default_output=format_output("[alarm_status]")) + def status(self) -> str: + """Return the alarm status from the device.""" + # Response: 'on', 'off', 'oning' + return self._gateway.send("get_arming").pop() + + @command(default_output=format_output("Turning alarm on")) + def on(self): + """Turn alarm on.""" + return self._gateway.send("set_arming", ["on"]) + + @command(default_output=format_output("Turning alarm off")) + def off(self): + """Turn alarm off.""" + return self._gateway.send("set_arming", ["off"]) + + @command() + def arming_time(self) -> int: + """ + Return time in seconds the alarm stays 'oning' + before transitioning to 'on' + """ + # Response: 5, 15, 30, 60 + return self._gateway.send("get_arm_wait_time").pop() + + @command(click.argument("seconds")) + def set_arming_time(self, seconds): + """Set time the alarm stays at 'oning' before transitioning to 'on'.""" + return self._gateway.send("set_arm_wait_time", [seconds]) + + @command() + def triggering_time(self) -> int: + """Return the time in seconds the alarm is going off when triggered.""" + # Response: 30, 60, etc. + return self._gateway.get_prop("alarm_time_len").pop() + + @command(click.argument("seconds")) + def set_triggering_time(self, seconds): + """Set the time in seconds the alarm is going off when triggered.""" + return self._gateway.set_prop("alarm_time_len", seconds) + + @command() + def triggering_light(self) -> int: + """ + Return the time the gateway light blinks + when the alarm is triggerd + """ + # Response: 0=do not blink, 1=always blink, x>1=blink for x seconds + return self._gateway.get_prop("en_alarm_light").pop() + + @command(click.argument("seconds")) + def set_triggering_light(self, seconds): + """Set the time the gateway light blinks when the alarm is triggerd.""" + # values: 0=do not blink, 1=always blink, x>1=blink for x seconds + return self._gateway.set_prop("en_alarm_light", seconds) + + @command() + def triggering_volume(self) -> int: + """Return the volume level at which alarms go off [0-100].""" + return self._gateway.send("get_alarming_volume").pop() + + @command(click.argument("volume")) + def set_triggering_volume(self, volume): + """Set the volume level at which alarms go off [0-100].""" + return self._gateway.send("set_alarming_volume", [volume]) + + @command() + def last_status_change_time(self) -> datetime: + """ + Return the last time the alarm changed status. + """ + return datetime.fromtimestamp(self._gateway.send("get_arming_time").pop()) diff --git a/miio/gateway/devices/__init__.py b/miio/gateway/devices/__init__.py new file mode 100644 index 000000000..24b3b6bec --- /dev/null +++ b/miio/gateway/devices/__init__.py @@ -0,0 +1,7 @@ +"""Xiaomi Gateway subdevice base class.""" + +# flake8: noqa +from .light import LightBulb +from .switch import Switch + +from .subdevice import SubDevice, SubDeviceInfo # isort:skip diff --git a/miio/gateway/devices/light.py b/miio/gateway/devices/light.py new file mode 100644 index 000000000..2f34eb740 --- /dev/null +++ b/miio/gateway/devices/light.py @@ -0,0 +1,43 @@ +"""Xiaomi Zigbee lights.""" + +import click + +from ...click_common import command +from .subdevice import SubDevice + + +class LightBulb(SubDevice): + """Base class for subdevice light bulbs.""" + + @command() + def update(self): + """Update all device properties.""" + self._props["brightness"] = self.send("get_bright").pop() + self._props["color_temp"] = self.send("get_ct").pop() + if self._props["brightness"] > 0 and self._props["brightness"] <= 100: + self._props["status"] = "on" + else: + self._props["status"] = "off" + + @command() + def on(self): + """Turn bulb on.""" + return self.send_arg("set_power", ["on"]).pop() + + @command() + def off(self): + """Turn bulb off.""" + return self.send_arg("set_power", ["off"]).pop() + + @command(click.argument("ctt", type=int)) + def set_color_temp(self, ctt): + """Set the color temperature of the bulb ctt_min-ctt_max.""" + return self.send_arg("set_ct", [ctt]).pop() + + @command(click.argument("brightness", type=int)) + def set_brightness(self, brightness): + """Set the brightness of the bulb 1-100.""" + return self.send_arg("set_bright", [brightness]).pop() + + + diff --git a/miio/gateway/devices/subdevice.py b/miio/gateway/devices/subdevice.py new file mode 100644 index 000000000..1678dfef5 --- /dev/null +++ b/miio/gateway/devices/subdevice.py @@ -0,0 +1,240 @@ +"""Xiaomi Gateway subdevice base class.""" + +import logging +from typing import Optional + +import attr +import click + +from ...click_common import command +from ..gateway import GATEWAY_MODEL_EU, GATEWAY_MODEL_ZIG3, Gateway, GatewayException + +_LOGGER = logging.getLogger(__name__) + + +@attr.s(auto_attribs=True) +class SubDeviceInfo: + """SubDevice discovery info.""" + + sid: str + type_id: int + unknown: int + unknown2: int + fw_ver: int + + +class SubDevice: + """ + Base class for all subdevices of the gateway + these devices are connected through zigbee. + """ + + def __init__( + self, + gw: Gateway = None, + dev_info: SubDeviceInfo = None, + model_info: dict = {}, + ) -> None: + + self._gw = gw + self.sid = dev_info.sid + self._model_info = model_info + self._battery = None + self._voltage = None + self._fw_ver = dev_info.fw_ver + + self._model = model_info.get("model", "unknown") + self._name = model_info.get("name", "unknown") + self._zigbee_model = model_info.get("zigbee_id", "unknown") + + self._props = {} + self.get_prop_exp_dict = {} + for prop in model_info.get("properties", []): + prop_name = prop.get("name", prop["property"]) + self._props[prop_name] = prop.get("default", None) + if prop.get("get") == "get_property_exp": + self.get_prop_exp_dict[prop["property"]] = prop + + self.setter = model_info.get("setter") + + def __repr__(self): + return "" % ( + self.device_type, + self.sid, + self.model, + self.zigbee_model, + self.firmware_version, + self.get_battery(), + self.get_voltage(), + self.status, + ) + + @property + def status(self): + """Return sub-device status as a dict containing all properties.""" + return self._props + + @property + def device_type(self): + """Return the device type name.""" + return self._model_info.get('type') + + @property + def name(self): + """Return the name of the device.""" + return f"{self._name} ({self.sid})" + + @property + def model(self): + """Return the device model.""" + return self._model + + @property + def zigbee_model(self): + """Return the zigbee device model.""" + return self._zigbee_model + + @property + def firmware_version(self): + """Return the firmware version.""" + return self._fw_ver + + @property + def battery(self): + """Return the battery level in %.""" + return self._battery + + @property + def voltage(self): + """Return the battery voltage in V.""" + return self._voltage + + @command() + def update(self): + """Update all device properties.""" + if self.get_prop_exp_dict: + values = self.get_property_exp(list(self.get_prop_exp_dict.keys())) + try: + i = 0 + for prop in self.get_prop_exp_dict.values(): + result = values[i] + if prop.get("devisor"): + result = values[i] / prop.get("devisor") + prop_name = prop.get("name", prop["property"]) + self._props[prop_name] = result + i = i+1 + except Exception as ex: + raise GatewayException( + "One or more unexpected results while " + "fetching properties %s: %s" % (self.get_prop_exp_dict, values) + ) from ex + + @command() + def send(self, command): + """Send a command/query to the subdevice.""" + try: + return self._gw.send(command, [self.sid]) + except Exception as ex: + raise GatewayException( + "Got an exception while sending command %s" % (command) + ) from ex + + @command() + def send_arg(self, command, arguments): + """Send a command/query including arguments to the subdevice.""" + try: + return self._gw.send(command, arguments, extra_parameters={"sid": self.sid}) + except Exception as ex: + raise GatewayException( + "Got an exception while sending " + "command '%s' with arguments '%s'" % (command, str(arguments)) + ) from ex + + @command(click.argument("property")) + def get_property(self, property): + """Get the value of a property of the subdevice.""" + try: + response = self._gw.send("get_device_prop", [self.sid, property]) + except Exception as ex: + raise GatewayException( + "Got an exception while fetching property %s" % (property) + ) from ex + + if not response: + raise GatewayException( + "Empty response while fetching property '%s': %s" % (property, response) + ) + + return response + + @command(click.argument("properties", nargs=-1)) + def get_property_exp(self, properties): + """Get the value of a bunch of properties of the subdevice.""" + try: + response = self._gw.send( + "get_device_prop_exp", [[self.sid] + list(properties)] + ).pop() + except Exception as ex: + raise GatewayException( + "Got an exception while fetching properties %s: %s" % (properties) + ) from ex + + if len(list(properties)) != len(response): + raise GatewayException( + "unexpected result while fetching properties %s: %s" + % (properties, response) + ) + + return response + + @command(click.argument("property"), click.argument("value")) + def set_property(self, property, value): + """Set a device property of the subdevice.""" + try: + return self._gw.send("set_device_prop", {"sid": self.sid, property: value}) + except Exception as ex: + raise GatewayException( + "Got an exception while setting propertie %s to value %s" + % (property, str(value)) + ) from ex + + @command() + def unpair(self): + """Unpair this device from the gateway.""" + return self.send("remove_device") + + @command() + def get_battery(self): + """Update the battery level, if available.""" + if self._gw.model not in [GATEWAY_MODEL_EU, GATEWAY_MODEL_ZIG3]: + self._battery = self.send("get_battery").pop() + else: + _LOGGER.info( + "Gateway model '%s' does not (yet) support get_battery", + self._gw.model, + ) + return self._battery + + @command() + def get_voltage(self): + """Update the battery voltage, if available.""" + if self._gw.model in [GATEWAY_MODEL_EU, GATEWAY_MODEL_ZIG3]: + self._voltage = self.get_property("voltage").pop() / 1000 + else: + _LOGGER.info( + "Gateway model '%s' does not (yet) support get_voltage", + self._gw.model, + ) + return self._voltage + + @command() + def get_firmware_version(self) -> Optional[int]: + """Returns firmware version.""" + try: + self._fw_ver = self.get_property("fw_ver").pop() + except Exception as ex: + _LOGGER.info( + "get_firmware_version failed, returning firmware version from discovery info: %s", + ex, + ) + return self._fw_ver diff --git a/miio/gateway/devices/subdevices.yaml b/miio/gateway/devices/subdevices.yaml new file mode 100644 index 000000000..0f9f6374a --- /dev/null +++ b/miio/gateway/devices/subdevices.yaml @@ -0,0 +1,629 @@ +# Default +- zigbee_id: unknown + model: unknown + type_id: -1 + name: unknown + type: unknown + class: SubDevice + +# Gateway +- zigbee_id: lumi.0 + model: Gateway + type_id: 0 + name: Gateway + type: Gateway + class: None + +# Weather sensor +- zigbee_id: lumi.sensor_ht.v1 + model: WSDCGQ01LM + type_id: 10 + name: Weather sensor + type: SensorHT + class: SubDevice + getter: get_prop_sensor_ht + properties: + - property: temperature + unit: degrees celsius + get: get_property_exp + devisor: 100 + - property: humidity + unit: percent + get: get_property_exp + devisor: 100 + +- zigbee_id: lumi.weather.v1 + model: WSDCGQ11LM + type_id: 19 + name: Weather sensor + type: AqaraHT + class: SubDevice + getter: get_prop_sensor_ht + properties: + - property: temperature + unit: degrees celsius + get: get_property_exp + devisor: 100 + - property: humidity + unit: percent + get: get_property_exp + devisor: 100 + - property: pressure + unit: hpa + get: get_property_exp + devisor: 100 + +# Door sensor +- zigbee_id: lumi.sensor_magnet + model: MCCGQ01LM + type_id: 3 + name: Door sensor + type: Magnet + class: SubDevice + +- zigbee_id: lumi.sensor_magnet.aq2 + model: MCCGQ11LM + type_id: 53 + name: Door sensor + type: AqaraMagnet + class: SubDevice + +# Motion sensor +- zigbee_id: lumi.sensor_motion + model: RTCGQ01LM + type_id: 2 + name: Motion sensor + type: Motion + class: SubDevice + +- zigbee_id: lumi.sensor_motion.aq2 + model: RTCGQ11LM + type_id: 52 + name: Motion sensor + type: AqaraMotion + class: SubDevice + +# Cube +- zigbee_id: lumi.sensor_cube.v1 + model: MFKZQ01LM + type_id: 8 + name: Cube + type: Cube + class: SubDevice + +- zigbee_id: lumi.sensor_cube.aqgl01 + model: MFKZQ01LM + type_id: 68 + name: Cube + type: CubeV2 + class: SubDevice + +# Curtain +- zigbee_id: lumi.curtain + model: ZNCLDJ11LM + type_id: 13 + name: Curtain + type: CurtainV1 + class: SubDevice + +- zigbee_id: lumi.curtain.aq2 + model: ZNGZDJ11LM + type_id: 71 + name: Curtain + type: Curtain + class: SubDevice + +- zigbee_id: lumi.curtain.hagl04 + model: ZNCLDJ12LM + type_id: 72 + name: Curtain B1 + type: CurtainB1 + class: SubDevice + +# LightBulb +- zigbee_id: lumi.light.aqcn02 + model: ZNLDP12LM + type_id: 66 + name: Smart bulb E27 + type: AqaraSmartBulbE27 + class: LightBulb + properties: + - property: status # 'on' / 'off' + - property: brightness + unit: percent + - property: color_temp + unit: cct + - property: cct_min + unit: cct + default: 153 + - property: cct_max + unit: cct + default: 500 + +- zigbee_id: ikea.light.led1545g12 + model: LED1545G12 + type_id: 82 + name: Ikea smart bulb E27 white + type: IkeaBulb82 + class: LightBulb + properties: + - property: status # 'on' / 'off' + - property: brightness + unit: percent + - property: color_temp + unit: cct + - property: cct_min + unit: cct + default: 153 + - property: cct_max + unit: cct + default: 500 + +- zigbee_id: ikea.light.led1546g12 + model: LED1546G12 + type_id: 83 + name: Ikea smart bulb E27 white + type: IkeaBulb83 + class: LightBulb + properties: + - property: status # 'on' / 'off' + - property: brightness + unit: percent + - property: color_temp + unit: cct + - property: cct_min + unit: cct + default: 153 + - property: cct_max + unit: cct + default: 500 + +- zigbee_id: ikea.light.led1536g5 + model: LED1536G5 + type_id: 84 + name: Ikea smart bulb E12 white + type: IkeaBulb84 + class: LightBulb + properties: + - property: status # 'on' / 'off' + - property: brightness + unit: percent + - property: color_temp + unit: cct + - property: cct_min + unit: cct + default: 153 + - property: cct_max + unit: cct + default: 500 + +- zigbee_id: ikea.light.led1537r6 + model: LED1537R6 + type_id: 85 + name: Ikea smart bulb GU10 white + type: IkeaBulb85 + class: LightBulb + properties: + - property: status # 'on' / 'off' + - property: brightness + unit: percent + - property: color_temp + unit: cct + - property: cct_min + unit: cct + default: 153 + - property: cct_max + unit: cct + default: 500 + +- zigbee_id: ikea.light.led1623g12 + model: LED1623G12 + type_id: 86 + name: Ikea smart bulb E27 white + type: IkeaBulb86 + class: LightBulb + properties: + - property: status # 'on' / 'off' + - property: brightness + unit: percent + - property: color_temp + unit: cct + - property: cct_min + unit: cct + default: 153 + - property: cct_max + unit: cct + default: 500 + +- zigbee_id: ikea.light.led1650r5 + model: LED1650R5 + type_id: 87 + name: Ikea smart bulb GU10 white + type: IkeaBulb87 + class: LightBulb + properties: + - property: status # 'on' / 'off' + - property: brightness + unit: percent + - property: color_temp + unit: cct + - property: cct_min + unit: cct + default: 153 + - property: cct_max + unit: cct + default: 500 + +- zigbee_id: ikea.light.led1649c5 + model: LED1649C5 + type_id: 88 + name: Ikea smart bulb E12 white + type: IkeaBulb88 + class: LightBulb + properties: + - property: status # 'on' / 'off' + - property: brightness + unit: percent + - property: color_temp + unit: cct + - property: cct_min + unit: cct + default: 153 + - property: cct_max + unit: cct + default: 500 + +# Lock +- zigbee_id: lumi.lock.aq1 + model: ZNMS11LM + type_id: 59 + name: Door lock S1 + type: DoorLockS1 + class: SubDevice + properties: + - property: status # 'locked' / 'unlocked' + +- zigbee_id: lumi.lock.acn02 + model: ZNMS12LM + type_id: 70 + name: Door lock S2 + type: LockS2 + class: SubDevice + properties: + - property: status # 'locked' / 'unlocked' + +- zigbee_id: lumi.lock.v1 + model: A6121 + type_id: 81 + name: Vima cylinder lock + type: LockV1 + class: SubDevice + properties: + - property: status # 'locked' / 'unlocked' + +- zigbee_id: lumi.lock.acn03 + model: ZNMS13LM + type_id: 163 + name: Door lock S2 pro + type: LockS2Pro + class: SubDevice + properties: + - property: status # 'locked' / 'unlocked' + +# Sensors +- zigbee_id: lumi.sensor_smoke + model: JTYJ-GD-01LM/BW + type_id: 15 + name: Honeywell smoke detector + type: SensorSmoke + class: SubDevice + +- zigbee_id: lumi.sensor_natgas + model: JTQJ-BF-01LM/BW + type_id: 18 + name: Honeywell natural gas detector + type: SensorNatgas + class: SubDevice + +- zigbee_id: lumi.sensor_wleak.aq1 + model: SJCGQ11LM + type_id: 55 + name: Water leak sensor + type: AqaraWaterLeak + class: SubDevice + +- zigbee_id: lumi.vibration.aq1 + model: DJT11LM + type_id: 56 + name: Vibration sensor + type: AqaraVibration + class: SubDevice + +# Thermostats +- zigbee_id: lumi.airrtc.tcpecn02 + model: KTWKQ03ES + type_id: 207 + name: Thermostat S2 + type: ThermostatS2 + class: SubDevice + +# Remote Switch +- zigbee_id: lumi.sensor_86sw2.v1 + model: WXKG02LM 2016 + type_id: 12 + name: Remote switch double + type: RemoteSwitchDoubleV1 + class: SubDevice + +- zigbee_id: lumi.sensor_86sw1.v1 + model: WXKG03LM 2016 + type_id: 14 + name: Remote switch single + type: RemoteSwitchSingleV1 + class: SubDevice + +- zigbee_id: lumi.remote.b186acn01 + model: WXKG03LM 2018 + type_id: 134 + name: Remote switch single + type: RemoteSwitchSingle + class: SubDevice + +- zigbee_id: lumi.remote.b286acn01 + model: WXKG02LM 2018 + type_id: 135 + name: Remote switch double + type: RemoteSwitchDouble + class: SubDevice + +- zigbee_id: lumi.remote.b186acn02 + model: WXKG06LM + type_id: 171 + name: D1 remote switch single + type: D1RemoteSwitchSingle + class: SubDevice + +- zigbee_id: lumi.remote.b286acn02 + model: WXKG07LM + type_id: 172 + name: D1 remote switch double + type: D1RemoteSwitchDouble + class: SubDevice + +- zigbee_id: lumi.sensor_switch + model: WXKG01LM + type_id: 1 + name: Button + type: Switch + class: SubDevice + +- zigbee_id: lumi.sensor_switch.aq2 + model: WXKG11LM 2015 + type_id: 51 + name: Button + type: AqaraSwitch + class: SubDevice + +- zigbee_id: lumi.sensor_switch.aq3 + model: WXKG12LM + type_id: 62 + name: Button + type: AqaraSquareButtonV3 + class: SubDevice + +- zigbee_id: lumi.remote.b1acn01 + model: WXKG11LM 2018 + type_id: 133 + name: Button + type: AqaraSquareButton + class: SubDevice + +# Switches +- zigbee_id: lumi.ctrl_neutral2 + model: QBKG03LM + type_id: 7 + name: Wall switch double no neutral + type: SwitchTwoChannels + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: neutral_1 # 'on' / 'off' + name: status_ch1 + get: get_property_exp + +- zigbee_id: lumi.ctrl_neutral1.v1 + model: QBKG04LM + type_id: 9 + name: Wall switch no neutral + type: SwitchOneChannel + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + +- zigbee_id: lumi.ctrl_ln1 + model: QBKG11LM + type_id: 20 + name: Wall switch single + type: SwitchLiveOneChannel + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + +- zigbee_id: lumi.ctrl_ln2 + model: QBKG12LM + type_id: 21 + name: Wall switch double + type: SwitchLiveTwoChannels + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: neutral_1 # 'on' / 'off' + name: status_ch1 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + +- zigbee_id: lumi.ctrl_ln1.aq1 + model: QBKG11LM + type_id: 63 + name: Wall switch single + type: AqaraSwitchOneChannel + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + +- zigbee_id: lumi.ctrl_ln2.aq1 + model: QBKG12LM + type_id: 64 + name: Wall switch double + type: AqaraSwitchTwoChannels + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: neutral_1 # 'on' / 'off' + name: status_ch1 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + +- zigbee_id: lumi.switch.n3acn3 + model: QBKG26LM + type_id: 176 + name: D1 wall switch triple + type: D1WallSwitchTriple + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: neutral_1 # 'on' / 'off' + name: status_ch1 + get: get_property_exp + - property: neutral_2 # 'on' / 'off' + name: status_ch2 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + +- zigbee_id: lumi.switch.l3acn3 + model: QBKG25LM + type_id: 177 + name: D1 wall switch triple no neutral + type: D1WallSwitchTripleNN + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: neutral_1 # 'on' / 'off' + name: status_ch1 + get: get_property_exp + - property: neutral_2 # 'on' / 'off' + name: status_ch2 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + +- zigbee_id: lumi.plug + model: ZNCZ02LM + type_id: 11 + name: Plug + type: Plug + class: Switch + getter: get_prop_plug + setter: toggle_plug + properties: + - property: neutral_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + +- zigbee_id: lumi.ctrl_86plug.v1 + model: QBCZ11LM + type_id: 17 + name: Wall outlet + type: AqaraWallOutletV1 + class: Switch + setter: toggle_plug + properties: + - property: channel_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + +- zigbee_id: lumi.ctrl_86plug.aq1 + model: QBCZ11LM + type_id: 65 + name: Wall outlet + type: AqaraWallOutlet + class: Switch + setter: toggle_plug + properties: + - property: channel_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + +- zigbee_id: lumi.relay.c2acn01 + model: LLKZMK11LM + type_id: 54 + name: Relay + type: AqaraRelayTwoChannels + class: Switch + setter: toggle_ctrl_neutral + properties: + - property: channel_0 # 'on' / 'off' + name: status_ch0 + get: get_property_exp + - property: channel_1 # 'on' / 'off' + name: status_ch1 + get: get_property_exp + - property: load_power + unit: Watt + get: get_property_exp + + +# from https://github.com/aholstenson/miio/issues/26 +# 166 - lumi.lock.acn05 +# 167 - lumi.switch.b1lacn02 +# 168 - lumi.switch.b2lacn02 +# 169 - lumi.switch.b1nacn02 +# 170 - lumi.switch.b2nacn02 +# 202 - lumi.dimmer.rgbegl01 +# 203 - lumi.dimmer.c3egl01 +# 204 - lumi.dimmer.cwegl01 +# 205 - lumi.airrtc.vrfegl01 +# 206 - lumi.airrtc.tcpecn01 diff --git a/miio/gateway/devices/switch.py b/miio/gateway/devices/switch.py new file mode 100644 index 000000000..a286642b9 --- /dev/null +++ b/miio/gateway/devices/switch.py @@ -0,0 +1,40 @@ +"""Xiaomi Zigbee switches.""" + +from enum import IntEnum + +import click + +from ...click_common import command +from .subdevice import SubDevice + + +class Switch(SubDevice): + """Base class for one channel switch subdevice that supports on/off.""" + + class ChannelMap(IntEnum): + """Option to select wich channel to control.""" + + channel_0 = 0 + channel_1 = 1 + channel_2 = 2 + + @command(click.argument("channel", type=int)) + def toggle(self, channel: int = 0): + """Toggle a channel of the switch, default channel_0.""" + return self.send_arg( + self.setter, [self.ChannelMap(channel).name, "toggle"] + ).pop() + + @command(click.argument("channel", type=int)) + def on(self, channel: int = 0): + """Turn on a channel of the switch, default channel_0.""" + return self.send_arg( + self.setter, [self.ChannelMap(channel).name, "on"] + ).pop() + + @command(click.argument("channel", type=int)) + def off(self, channel: int = 0): + """Turn off a channel of the switch, default channel_0.""" + return self.send_arg( + self.setter, [self.ChannelMap(channel).name, "off"] + ).pop() diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py new file mode 100644 index 000000000..e46b4d7e4 --- /dev/null +++ b/miio/gateway/gateway.py @@ -0,0 +1,324 @@ +"""Xiaomi Gateway implementation using Miio protecol.""" + +import logging + +import os +import sys +import click +import yaml + +from ..click_common import command +from ..device import Device +from ..exceptions import DeviceError, DeviceException + +_LOGGER = logging.getLogger(__name__) + +GATEWAY_MODEL_CHINA = "lumi.gateway.v3" +GATEWAY_MODEL_EU = "lumi.gateway.mieu01" +GATEWAY_MODEL_ZIG3 = "lumi.gateway.mgl03" +GATEWAY_MODEL_AQARA = "lumi.gateway.aqhm01" +GATEWAY_MODEL_AC_V1 = "lumi.acpartner.v1" +GATEWAY_MODEL_AC_V2 = "lumi.acpartner.v2" +GATEWAY_MODEL_AC_V3 = "lumi.acpartner.v3" + +class GatewayException(DeviceException): + """Exception for the Xioami Gateway communication.""" + + +class Gateway(Device): + """Main class representing the Xiaomi Gateway. + + Use the given property getters to access specific functionalities such + as `alarm` (for alarm controls) or `light` (for lights). + + Commands whose functionality or parameters are unknown, + feel free to implement! + * toggle_device + * toggle_plug + * remove_all_bind + * list_bind [0] + * bind_page + * bind + * remove_bind + + * self.get_prop("used_for_public") # Return the 'used_for_public' status, return value: [0] or [1], probably this has to do with developer mode. + * self.set_prop("used_for_public", state) # Set the 'used_for_public' state, value: 0 or 1, probably this has to do with developer mode. + + * welcome + * set_curtain_level + + * get_corridor_on_time + * set_corridor_light ["off"] + * get_corridor_light -> "on" + + * set_default_sound + * set_doorbell_push, get_doorbell_push ["off"] + * set_doorbell_volume [100], get_doorbell_volume + * set_gateway_volume, get_gateway_volume + * set_clock_volume + * set_clock + * get_sys_data + * update_neighbor_token [{"did":x, "token":x, "ip":x}] + + ## property getters + * ctrl_device_prop + * get_device_prop_exp [[sid, list, of, properties]] + + ## scene + * get_lumi_bind ["scene", ] for rooms/devices""" + + def __init__( + self, + ip: str = None, + token: str = None, + start_id: int = 0, + debug: int = 0, + lazy_discover: bool = True, + ) -> None: + super().__init__(ip, token, start_id, debug, lazy_discover) + + from . import ( + Alarm, + Radio, + Zigbee, + Light, + ) + + self._alarm = Alarm(parent=self) + self._radio = Radio(parent=self) + self._zigbee = Zigbee(parent=self) + self._light = Light(parent=self) + self._devices = {} + self._info = None + self._subdevice_model_map = None + + def _get_subdevice_model_map(self): + if self._subdevice_model_map is None: + filedata = open(os.path.dirname(__file__) + '\devices\subdevices.yaml', 'r') + self._subdevice_model_map = yaml.safe_load(filedata) + return self._subdevice_model_map + + def _get_unknown_model(self): + self._get_subdevice_model_map() + + for model_info in self._subdevice_model_map: + if model_info.get('type_id') == -1: + return model_info + + @property + def alarm(self) -> "GatewayAlarm": # noqa: F821 + """Return alarm control interface.""" + # example: gateway.alarm.on() + return self._alarm + + @property + def radio(self) -> "GatewayRadio": # noqa: F821 + """Return radio control interface.""" + return self._radio + + @property + def zigbee(self) -> "GatewayZigbee": # noqa: F821 + """Return zigbee control interface.""" + return self._zigbee + + @property + def light(self) -> "GatewayLight": # noqa: F821 + """Return light control interface.""" + return self._light + + @property + def devices(self): + """Return a dict of the already discovered devices.""" + return self._devices + + @property + def model(self): + """Return the zigbee model of the gateway.""" + # Check if catch already has the gateway info, otherwise get it from the device + if self._info is None: + self._info = self.info() + return self._info.model + + @command() + def discover_devices(self): + """ + Discovers SubDevices + and returns a list of the discovered devices. + """ + + from .devices import SubDeviceInfo + + self._devices = {} + + # Skip the models which do not support getting the device list + if self.model == GATEWAY_MODEL_EU: + _LOGGER.warning( + "Gateway model '%s' does not (yet) support getting the device list", + self.model, + ) + return self._devices + + if self.model == GATEWAY_MODEL_ZIG3: + # self.get_prop("device_list") does not work for the GATEWAY_MODEL_ZIG3 + # self.send("get_device_list") does work for the GATEWAY_MODEL_ZIG3 but gives slightly diffrent return values + devices_raw = self.send("get_device_list") + + for device in devices_raw: + # Match 'model' to get the model_info + model_info = self.match_zigbee_model(device["model"], device["did"]) + + # Extract discovered information + dev_info = SubDeviceInfo(device["did"], model_info["type_id"], -1, -1, -1) + + # Setup the device + self.setup_device(dev_info, model_info) + else: + devices_raw = self.get_prop("device_list") + + for x in range(0, len(devices_raw), 5): + # Extract discovered information + dev_info = SubDeviceInfo(*devices_raw[x : x + 5]) + + # Match 'type_id' to get the model_info + model_info = self.match_type_id(dev_info.type_id, dev_info.sid) + + # Setup the device + self.setup_device(dev_info, model_info) + + return self._devices + + @command(click.argument("zigbee_model", "sid")) + def match_zigbee_model(self, zigbee_model, sid): + """ + Match the zigbee_model to obtain the model_info + """ + + self._get_subdevice_model_map() + + for model_info in self._subdevice_model_map: + if model_info.get('zigbee_id') == zigbee_model: + return model_info + + _LOGGER.warning( + "Unknown subdevice discovered, could not match zigbee_model '%s' " + "of subdevice sid '%s' from Xiaomi gateway with ip: %s", + zigbee_model, + sid, + self.ip, + ) + return self._get_unknown_model() + + @command(click.argument("type_id", "sid")) + def match_type_id(self, type_id, sid): + """ + Match the type_id to obtain the model_info + """ + + self._get_subdevice_model_map() + + for model_info in self._subdevice_model_map: + if model_info.get('type_id') == type_id: + return model_info + + _LOGGER.warning( + "Unknown subdevice discovered, could not match type_id '%i' " + "of subdevice sid '%s' from Xiaomi gateway with ip: %s", + type_id, + sid, + self.ip, + ) + return self._get_unknown_model() + + @command(click.argument("dev_info", "model_info")) + def setup_device(self, dev_info, model_info): + """ + Setup a device using the SubDeviceInfo and model_info + """ + + from .devices import SubDevice + + if model_info.get('type') == "Gateway": + # ignore the gateway itself + return + + # Obtain the correct subdevice class + subdevice_cls = getattr(sys.modules['miio.gateway.devices'], model_info.get("class")) + if subdevice_cls is None: + subdevice_cls = SubDevice + _LOGGER.info( + "Gateway device type '%s' " + "does not have device specific methods defined, " + "only basic default methods will be available", + model_info.get('type'), + ) + + # Initialize and save the subdevice + self._devices[dev_info.sid] = subdevice_cls(self, dev_info, model_info) + if self._devices[dev_info.sid].status == {}: + _LOGGER.info( + "Discovered subdevice type '%s', has no device specific properties defined, " + "this device has not been fully implemented yet (model: %s, name: %s).", + model_info.get('type'), + self._devices[dev_info.sid].model, + self._devices[dev_info.sid].name, + ) + + return self._devices[dev_info.sid] + + @command(click.argument("property")) + def get_prop(self, property): + """Get the value of a property for given sid.""" + return self.send("get_device_prop", ["lumi.0", property]) + + @command(click.argument("properties", nargs=-1)) + def get_prop_exp(self, properties): + """Get the value of a bunch of properties for given sid.""" + return self.send("get_device_prop_exp", [["lumi.0"] + list(properties)]) + + @command(click.argument("property"), click.argument("value")) + def set_prop(self, property, value): + """Set the device property.""" + return self.send("set_device_prop", {"sid": "lumi.0", property: value}) + + @command() + def clock(self): + """Alarm clock""" + # payload of clock volume ("get_clock_volume") + # already in get_clock response + return self.send("get_clock") + + # Developer key + @command() + def get_developer_key(self): + """Return the developer API key.""" + return self.send("get_lumi_dpf_aes_key")[0] + + @command(click.argument("key")) + def set_developer_key(self, key): + """Set the developer API key.""" + if len(key) != 16: + click.echo("Key must be of length 16, was %s" % len(key)) + + return self.send("set_lumi_dpf_aes_key", [key]) + + @command() + def enable_telnet(self): + """Enable root telnet acces to the operating system, use login "admin" or "app", no password.""" + try: + return self.send("enable_telnet_service") + except DeviceError: + _LOGGER.error( + "Gateway model '%s' does not (yet) support enabling the telnet interface", + self.model, + ) + return None + + @command() + def timezone(self): + """Get current timezone.""" + return self.get_prop("tzone_sec") + + @command() + def get_illumination(self): + """Get illumination. In lux?""" + return self.send("get_illumination").pop() diff --git a/miio/gateway/gatewaydevice.py b/miio/gateway/gatewaydevice.py new file mode 100644 index 000000000..be2dcb48e --- /dev/null +++ b/miio/gateway/gatewaydevice.py @@ -0,0 +1,32 @@ +"""Xiaomi Gateway device base class.""" + +import logging + +from ..device import Device +from .gateway import Gateway + +_LOGGER = logging.getLogger(__name__) + + +class GatewayDevice(Device): + """ + GatewayDevice class + Specifies the init method for all gateway device functionalities. + """ + + def __init__( + self, + ip: str = None, + token: str = None, + start_id: int = 0, + debug: int = 0, + lazy_discover: bool = True, + parent: Gateway = None, + ) -> None: + if parent is not None: + self._gateway = parent + else: + self._gateway = Device(ip, token, start_id, debug, lazy_discover) + _LOGGER.debug( + "Creating new device instance, only use this for cli interface" + ) diff --git a/miio/gateway/light.py b/miio/gateway/light.py new file mode 100644 index 000000000..8fb7a261d --- /dev/null +++ b/miio/gateway/light.py @@ -0,0 +1,162 @@ +"""Xiaomi Gateway Light implementation.""" + +from typing import Tuple + +import click + +from ..click_common import command +from ..utils import brightness_and_color_to_int, int_to_brightness, int_to_rgb +from .gatewaydevice import GatewayDevice + +color_map = { + "red": (255, 0, 0), + "green": (0, 255, 0), + "blue": (0, 0, 255), + "white": (255, 255, 255), + "yellow": (255, 255, 0), + "orange": (255, 165, 0), + "aqua": (0, 255, 255), + "olive": (128, 128, 0), + "purple": (128, 0, 128), +} + + +class Light(GatewayDevice): + """ + Light controls for the gateway. + + The gateway LEDs can be controlled using 'rgb' or 'night_light' methods. + The 'night_light' methods control the same light as the 'rgb' methods, but has a separate memory for brightness and color. + Changing the 'rgb' light does not affect the stored state of the 'night_light', while changing the 'night_light' does effect the state of the 'rgb' light. + """ + + @command() + def rgb_status(self): + """ + Get current status of the light. + Always represents the current status of the light as opposed to 'night_light_status'. + + Example: + {"is_on": false, "brightness": 0, "rgb": (0, 0, 0)} + """ + # Returns {"is_on": false, "brightness": 0, "rgb": (0, 0, 0)} when light is off + state_int = self._gateway.send("get_rgb").pop() + brightness = int_to_brightness(state_int) + rgb = int_to_rgb(state_int) + is_on = brightness > 0 + + return {"is_on": is_on, "brightness": brightness, "rgb": rgb} + + @command() + def night_light_status(self): + """ + Get status of the night light. + This command only gives the correct status of the LEDs if the last command was a 'night_light' command and not a 'rgb' light command, otherwise it gives the stored values of the 'night_light'. + + Example: + {"is_on": false, "brightness": 0, "rgb": (0, 0, 0)} + """ + state_int = self._gateway.send("get_night_light_rgb").pop() + brightness = int_to_brightness(state_int) + rgb = int_to_rgb(state_int) + is_on = brightness > 0 + + return {"is_on": is_on, "brightness": brightness, "rgb": rgb} + + @command( + click.argument("brightness", type=int), + click.argument("rgb", type=(int, int, int)), + ) + def set_rgb(self, brightness: int, rgb: Tuple[int, int, int]): + """Set gateway light using brightness and rgb tuple.""" + brightness_and_color = brightness_and_color_to_int(brightness, rgb) + + return self._gateway.send("set_rgb", [brightness_and_color]) + + @command( + click.argument("brightness", type=int), + click.argument("rgb", type=(int, int, int)), + ) + def set_night_light(self, brightness: int, rgb: Tuple[int, int, int]): + """Set gateway night light using brightness and rgb tuple.""" + brightness_and_color = brightness_and_color_to_int(brightness, rgb) + + return self._gateway.send("set_night_light_rgb", [brightness_and_color]) + + @command(click.argument("brightness", type=int)) + def set_rgb_brightness(self, brightness: int): + """Set gateway light brightness (0-100).""" + if 100 < brightness < 0: + raise Exception("Brightness must be between 0 and 100") + current_color = self.rgb_status()["rgb"] + + return self.set_rgb(brightness, current_color) + + @command(click.argument("brightness", type=int)) + def set_night_light_brightness(self, brightness: int): + """Set night light brightness (0-100).""" + if 100 < brightness < 0: + raise Exception("Brightness must be between 0 and 100") + current_color = self.night_light_status()["rgb"] + + return self.set_night_light(brightness, current_color) + + @command(click.argument("color_name", type=str)) + def set_rgb_color(self, color_name: str): + """Set gateway light color using color name ('color_map' variable in the source holds the valid values).""" + if color_name not in color_map.keys(): + raise Exception( + "Cannot find {color} in {colors}".format( + color=color_name, colors=color_map.keys() + ) + ) + current_brightness = self.rgb_status()["brightness"] + + return self.set_rgb(current_brightness, color_map[color_name]) + + @command(click.argument("color_name", type=str)) + def set_night_light_color(self, color_name: str): + """Set night light color using color name ('color_map' variable in the source holds the valid values).""" + if color_name not in color_map.keys(): + raise Exception( + "Cannot find {color} in {colors}".format( + color=color_name, colors=color_map.keys() + ) + ) + current_brightness = self.night_light_status()["brightness"] + + return self.set_night_light(current_brightness, color_map[color_name]) + + @command( + click.argument("color_name", type=str), + click.argument("brightness", type=int), + ) + def set_rgb_using_name(self, color_name: str, brightness: int): + """Set gateway light color (using color name, 'color_map' variable in the source holds the valid values) and brightness (0-100).""" + if 100 < brightness < 0: + raise Exception("Brightness must be between 0 and 100") + if color_name not in color_map.keys(): + raise Exception( + "Cannot find {color} in {colors}".format( + color=color_name, colors=color_map.keys() + ) + ) + + return self.set_rgb(brightness, color_map[color_name]) + + @command( + click.argument("color_name", type=str), + click.argument("brightness", type=int), + ) + def set_night_light_using_name(self, color_name: str, brightness: int): + """Set night light color (using color name, 'color_map' variable in the source holds the valid values) and brightness (0-100).""" + if 100 < brightness < 0: + raise Exception("Brightness must be between 0 and 100") + if color_name not in color_map.keys(): + raise Exception( + "Cannot find {color} in {colors}".format( + color=color_name, colors=color_map.keys() + ) + ) + + return self.set_night_light(brightness, color_map[color_name]) diff --git a/miio/gateway/radio.py b/miio/gateway/radio.py new file mode 100644 index 000000000..630226f12 --- /dev/null +++ b/miio/gateway/radio.py @@ -0,0 +1,113 @@ +"""Xiaomi Gateway Radio implementation.""" + +import click + +from ..click_common import command +from .gatewaydevice import GatewayDevice + + +class Radio(GatewayDevice): + """Radio controls for the gateway.""" + + @command() + def get_radio_info(self): + """Radio play info.""" + return self._gateway.send("get_prop_fm") + + @command(click.argument("volume")) + def set_radio_volume(self, volume): + """Set radio volume.""" + return self._gateway.send("set_fm_volume", [volume]) + + def play_music_new(self): + """Unknown.""" + # {'from': '4', 'id': 9514, + # 'method': 'set_default_music', 'params': [2, '21']} + # {'from': '4', 'id': 9515, + # 'method': 'play_music_new', 'params': ['21', 0]} + raise NotImplementedError() + + def play_specify_fm(self): + """play specific stream?""" + raise NotImplementedError() + # {"from": "4", "id": 65055, "method": "play_specify_fm", + # "params": {"id": 764, "type": 0, + # "url": "http://live.xmcdn.com/live/764/64.m3u8"}} + return self._gateway.send("play_specify_fm") + + def play_fm(self): + """radio on/off?""" + raise NotImplementedError() + # play_fm","params":["off"]} + return self._gateway.send("play_fm") + + def volume_ctrl_fm(self): + """Unknown.""" + raise NotImplementedError() + return self._gateway.send("volume_ctrl_fm") + + def get_channels(self): + """Unknown.""" + raise NotImplementedError() + # "method": "get_channels", "params": {"start": 0}} + return self._gateway.send("get_channels") + + def add_channels(self): + """Unknown.""" + raise NotImplementedError() + return self._gateway.send("add_channels") + + def remove_channels(self): + """Unknown.""" + raise NotImplementedError() + return self._gateway.send("remove_channels") + + def get_default_music(self): + """seems to timeout (w/o internet).""" + # params [0,1,2] + raise NotImplementedError() + return self._gateway.send("get_default_music") + + @command() + def get_music_info(self): + """Unknown.""" + info = self._gateway.send("get_music_info") + click.echo("info: %s" % info) + free_space = self._gateway.send("get_music_free_space") + click.echo("free space: %s" % free_space) + + @command() + def get_mute(self): + """mute of what?""" + return self._gateway.send("get_mute") + + def download_music(self): + """Unknown.""" + raise NotImplementedError() + return self._gateway.send("download_music") + + def delete_music(self): + """delete music.""" + raise NotImplementedError() + return self._gateway.send("delete_music") + + def download_user_music(self): + """Unknown.""" + raise NotImplementedError() + return self._gateway.send("download_user_music") + + def get_download_progress(self): + """progress for music downloads or updates?""" + # returns [':0'] + raise NotImplementedError() + return self._gateway.send("get_download_progress") + + @command() + def set_sound_playing(self): + """stop playing?""" + return self._gateway.send("set_sound_playing", ["off"]) + + def set_default_music(self): + """Unknown.""" + raise NotImplementedError() + # method":"set_default_music","params":[0,"2"]} diff --git a/miio/gateway/zigbee.py b/miio/gateway/zigbee.py new file mode 100644 index 000000000..0027a0d8e --- /dev/null +++ b/miio/gateway/zigbee.py @@ -0,0 +1,60 @@ +"""Xiaomi Gateway Zigbee control implementation.""" + +import click + +from ..click_common import command +from .gatewaydevice import GatewayDevice + + +class Zigbee(GatewayDevice): + """Zigbee controls.""" + + @command() + def get_zigbee_version(self): + """timeouts on device.""" + return self._gateway.send("get_zigbee_device_version") + + @command() + def get_zigbee_channel(self): + """Return currently used zigbee channel.""" + return self._gateway.send("get_zigbee_channel")[0] + + @command(click.argument("channel")) + def set_zigbee_channel(self, channel): + """Set zigbee channel.""" + return self._gateway.send("set_zigbee_channel", [channel]) + + @command(click.argument("timeout", type=int)) + def zigbee_pair(self, timeout): + """Start pairing, use 0 to disable.""" + return self._gateway.send("start_zigbee_join", [timeout]) + + def send_to_zigbee(self): + """How does this differ from writing? Unknown.""" + raise NotImplementedError() + return self._gateway.send("send_to_zigbee") + + def read_zigbee_eep(self): + """Read eeprom?""" + raise NotImplementedError() + return self._gateway.send("read_zig_eep", [0]) # 'ok' + + def read_zigbee_attribute(self): + """Read zigbee data?""" + raise NotImplementedError() + return self._gateway.send("read_zigbee_attribute", [0x0000, 0x0080]) + + def write_zigbee_attribute(self): + """Unknown parameters.""" + raise NotImplementedError() + return self._gateway.send("write_zigbee_attribute") + + @command() + def zigbee_unpair_all(self): + """Unpair all devices.""" + return self._gateway.send("remove_all_device") + + def zigbee_unpair(self, sid): + """Unpair a device.""" + # get a device obj an call dev.unpair() + raise NotImplementedError() From 2e1a7d4e83d29166691f509a5a69ff697b927e0c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 30 Jan 2021 21:38:26 +0100 Subject: [PATCH 02/18] re-apply already merged commit --- miio/gateway/gateway.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index e46b4d7e4..17a556cff 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -321,4 +321,9 @@ def timezone(self): @command() def get_illumination(self): """Get illumination. In lux?""" - return self.send("get_illumination").pop() + try: + return self.send("get_illumination").pop() + except Exception as ex: + raise GatewayException( + "Got an exception while getting gateway illumination" + ) from ex From 41b92aaaeeadf77935cb6c0005e4bda811cdb472 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 30 Jan 2021 21:46:04 +0100 Subject: [PATCH 03/18] black formatting --- miio/gateway/devices/light.py | 3 --- miio/gateway/devices/subdevice.py | 42 +++++++++++++++---------------- miio/gateway/devices/switch.py | 8 ++---- miio/gateway/gateway.py | 33 +++++++++++++----------- 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/miio/gateway/devices/light.py b/miio/gateway/devices/light.py index 2f34eb740..fa367e761 100644 --- a/miio/gateway/devices/light.py +++ b/miio/gateway/devices/light.py @@ -38,6 +38,3 @@ def set_color_temp(self, ctt): def set_brightness(self, brightness): """Set the brightness of the bulb 1-100.""" return self.send_arg("set_bright", [brightness]).pop() - - - diff --git a/miio/gateway/devices/subdevice.py b/miio/gateway/devices/subdevice.py index 1678dfef5..42b79c228 100644 --- a/miio/gateway/devices/subdevice.py +++ b/miio/gateway/devices/subdevice.py @@ -30,10 +30,7 @@ class SubDevice: """ def __init__( - self, - gw: Gateway = None, - dev_info: SubDeviceInfo = None, - model_info: dict = {}, + self, gw: Gateway = None, dev_info: SubDeviceInfo = None, model_info: dict = {}, ) -> None: self._gw = gw @@ -42,11 +39,11 @@ def __init__( self._battery = None self._voltage = None self._fw_ver = dev_info.fw_ver - + self._model = model_info.get("model", "unknown") self._name = model_info.get("name", "unknown") self._zigbee_model = model_info.get("zigbee_id", "unknown") - + self._props = {} self.get_prop_exp_dict = {} for prop in model_info.get("properties", []): @@ -58,15 +55,18 @@ def __init__( self.setter = model_info.get("setter") def __repr__(self): - return "" % ( - self.device_type, - self.sid, - self.model, - self.zigbee_model, - self.firmware_version, - self.get_battery(), - self.get_voltage(), - self.status, + return ( + "" + % ( + self.device_type, + self.sid, + self.model, + self.zigbee_model, + self.firmware_version, + self.get_battery(), + self.get_voltage(), + self.status, + ) ) @property @@ -77,7 +77,7 @@ def status(self): @property def device_type(self): """Return the device type name.""" - return self._model_info.get('type') + return self._model_info.get("type") @property def name(self): @@ -112,7 +112,7 @@ def voltage(self): @command() def update(self): """Update all device properties.""" - if self.get_prop_exp_dict: + if self.get_prop_exp_dict: values = self.get_property_exp(list(self.get_prop_exp_dict.keys())) try: i = 0 @@ -122,7 +122,7 @@ def update(self): result = values[i] / prop.get("devisor") prop_name = prop.get("name", prop["property"]) self._props[prop_name] = result - i = i+1 + i = i + 1 except Exception as ex: raise GatewayException( "One or more unexpected results while " @@ -210,8 +210,7 @@ def get_battery(self): self._battery = self.send("get_battery").pop() else: _LOGGER.info( - "Gateway model '%s' does not (yet) support get_battery", - self._gw.model, + "Gateway model '%s' does not (yet) support get_battery", self._gw.model, ) return self._battery @@ -222,8 +221,7 @@ def get_voltage(self): self._voltage = self.get_property("voltage").pop() / 1000 else: _LOGGER.info( - "Gateway model '%s' does not (yet) support get_voltage", - self._gw.model, + "Gateway model '%s' does not (yet) support get_voltage", self._gw.model, ) return self._voltage diff --git a/miio/gateway/devices/switch.py b/miio/gateway/devices/switch.py index a286642b9..e572303d5 100644 --- a/miio/gateway/devices/switch.py +++ b/miio/gateway/devices/switch.py @@ -28,13 +28,9 @@ def toggle(self, channel: int = 0): @command(click.argument("channel", type=int)) def on(self, channel: int = 0): """Turn on a channel of the switch, default channel_0.""" - return self.send_arg( - self.setter, [self.ChannelMap(channel).name, "on"] - ).pop() + return self.send_arg(self.setter, [self.ChannelMap(channel).name, "on"]).pop() @command(click.argument("channel", type=int)) def off(self, channel: int = 0): """Turn off a channel of the switch, default channel_0.""" - return self.send_arg( - self.setter, [self.ChannelMap(channel).name, "off"] - ).pop() + return self.send_arg(self.setter, [self.ChannelMap(channel).name, "off"]).pop() diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index 17a556cff..1c08f42a2 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -21,6 +21,7 @@ GATEWAY_MODEL_AC_V2 = "lumi.acpartner.v2" GATEWAY_MODEL_AC_V3 = "lumi.acpartner.v3" + class GatewayException(DeviceException): """Exception for the Xioami Gateway communication.""" @@ -94,15 +95,15 @@ def __init__( def _get_subdevice_model_map(self): if self._subdevice_model_map is None: - filedata = open(os.path.dirname(__file__) + '\devices\subdevices.yaml', 'r') + filedata = open(os.path.dirname(__file__) + "\devices\subdevices.yaml", "r") self._subdevice_model_map = yaml.safe_load(filedata) return self._subdevice_model_map def _get_unknown_model(self): self._get_subdevice_model_map() - + for model_info in self._subdevice_model_map: - if model_info.get('type_id') == -1: + if model_info.get("type_id") == -1: return model_info @property @@ -168,7 +169,9 @@ def discover_devices(self): model_info = self.match_zigbee_model(device["model"], device["did"]) # Extract discovered information - dev_info = SubDeviceInfo(device["did"], model_info["type_id"], -1, -1, -1) + dev_info = SubDeviceInfo( + device["did"], model_info["type_id"], -1, -1, -1 + ) # Setup the device self.setup_device(dev_info, model_info) @@ -192,11 +195,11 @@ def match_zigbee_model(self, zigbee_model, sid): """ Match the zigbee_model to obtain the model_info """ - + self._get_subdevice_model_map() - + for model_info in self._subdevice_model_map: - if model_info.get('zigbee_id') == zigbee_model: + if model_info.get("zigbee_id") == zigbee_model: return model_info _LOGGER.warning( @@ -213,11 +216,11 @@ def match_type_id(self, type_id, sid): """ Match the type_id to obtain the model_info """ - + self._get_subdevice_model_map() - + for model_info in self._subdevice_model_map: - if model_info.get('type_id') == type_id: + if model_info.get("type_id") == type_id: return model_info _LOGGER.warning( @@ -237,19 +240,21 @@ def setup_device(self, dev_info, model_info): from .devices import SubDevice - if model_info.get('type') == "Gateway": + if model_info.get("type") == "Gateway": # ignore the gateway itself return # Obtain the correct subdevice class - subdevice_cls = getattr(sys.modules['miio.gateway.devices'], model_info.get("class")) + subdevice_cls = getattr( + sys.modules["miio.gateway.devices"], model_info.get("class") + ) if subdevice_cls is None: subdevice_cls = SubDevice _LOGGER.info( "Gateway device type '%s' " "does not have device specific methods defined, " "only basic default methods will be available", - model_info.get('type'), + model_info.get("type"), ) # Initialize and save the subdevice @@ -258,7 +263,7 @@ def setup_device(self, dev_info, model_info): _LOGGER.info( "Discovered subdevice type '%s', has no device specific properties defined, " "this device has not been fully implemented yet (model: %s, name: %s).", - model_info.get('type'), + model_info.get("type"), self._devices[dev_info.sid].model, self._devices[dev_info.sid].name, ) From d5f08981d0811fa829f561db45464b82d9fe6f47 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 30 Jan 2021 21:52:39 +0100 Subject: [PATCH 04/18] fix isort and flake8 --- miio/gateway/gateway.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index 1c08f42a2..6816fef5f 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -1,9 +1,9 @@ """Xiaomi Gateway implementation using Miio protecol.""" import logging - import os import sys + import click import yaml @@ -95,7 +95,7 @@ def __init__( def _get_subdevice_model_map(self): if self._subdevice_model_map is None: - filedata = open(os.path.dirname(__file__) + "\devices\subdevices.yaml", "r") + filedata = open(os.path.dirname(__file__) + "/devices/subdevices.yaml", "r") self._subdevice_model_map = yaml.safe_load(filedata) return self._subdevice_model_map From 6a57c24f70f4af656c1dadde1f474776d95aab03 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 30 Jan 2021 22:00:25 +0100 Subject: [PATCH 05/18] fix black --- miio/gateway/devices/subdevice.py | 32 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/miio/gateway/devices/subdevice.py b/miio/gateway/devices/subdevice.py index 42b79c228..0d380aeb5 100644 --- a/miio/gateway/devices/subdevice.py +++ b/miio/gateway/devices/subdevice.py @@ -30,7 +30,10 @@ class SubDevice: """ def __init__( - self, gw: Gateway = None, dev_info: SubDeviceInfo = None, model_info: dict = {}, + self, + gw: Gateway = None, + dev_info: SubDeviceInfo = None, + model_info: dict = {}, ) -> None: self._gw = gw @@ -55,18 +58,15 @@ def __init__( self.setter = model_info.get("setter") def __repr__(self): - return ( - "" - % ( - self.device_type, - self.sid, - self.model, - self.zigbee_model, - self.firmware_version, - self.get_battery(), - self.get_voltage(), - self.status, - ) + return "" % ( + self.device_type, + self.sid, + self.model, + self.zigbee_model, + self.firmware_version, + self.get_battery(), + self.get_voltage(), + self.status, ) @property @@ -210,7 +210,8 @@ def get_battery(self): self._battery = self.send("get_battery").pop() else: _LOGGER.info( - "Gateway model '%s' does not (yet) support get_battery", self._gw.model, + "Gateway model '%s' does not (yet) support get_battery", + self._gw.model, ) return self._battery @@ -221,7 +222,8 @@ def get_voltage(self): self._voltage = self.get_property("voltage").pop() / 1000 else: _LOGGER.info( - "Gateway model '%s' does not (yet) support get_voltage", self._gw.model, + "Gateway model '%s' does not (yet) support get_voltage", + self._gw.model, ) return self._voltage From 7c7e966ba6d7fe39e951e05f075fee008f93c632 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 30 Jan 2021 22:05:47 +0100 Subject: [PATCH 06/18] fix issort --- miio/gateway/gateway.py | 1 + 1 file changed, 1 insertion(+) diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index 6816fef5f..58228d326 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -5,6 +5,7 @@ import sys import click + import yaml from ..click_common import command From 5fc176a77ef7a42ed5b345c8622bc97f32508f37 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 31 Jan 2021 11:24:37 +0100 Subject: [PATCH 07/18] fix magnet sensor zigbee id --- miio/gateway/devices/subdevices.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/gateway/devices/subdevices.yaml b/miio/gateway/devices/subdevices.yaml index 0f9f6374a..35e6282c8 100644 --- a/miio/gateway/devices/subdevices.yaml +++ b/miio/gateway/devices/subdevices.yaml @@ -54,7 +54,7 @@ devisor: 100 # Door sensor -- zigbee_id: lumi.sensor_magnet +- zigbee_id: lumi.sensor_magnet.v2 model: MCCGQ01LM type_id: 3 name: Door sensor From dc43f15d8c784548e7f05ccd34982720a6b3fd47 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 31 Jan 2021 15:26:27 +0100 Subject: [PATCH 08/18] update zigbee ids --- miio/gateway/devices/subdevices.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/gateway/devices/subdevices.yaml b/miio/gateway/devices/subdevices.yaml index 35e6282c8..107f9f09f 100644 --- a/miio/gateway/devices/subdevices.yaml +++ b/miio/gateway/devices/subdevices.yaml @@ -69,7 +69,7 @@ class: SubDevice # Motion sensor -- zigbee_id: lumi.sensor_motion +- zigbee_id: lumi.sensor_motion.v2 model: RTCGQ01LM type_id: 2 name: Motion sensor @@ -390,7 +390,7 @@ type: D1RemoteSwitchDouble class: SubDevice -- zigbee_id: lumi.sensor_switch +- zigbee_id: lumi.sensor_switch.v2 model: WXKG01LM type_id: 1 name: Button From 4fe1f37e536479047e89d830ab8a46e738a64265 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 4 Feb 2021 12:27:48 +0100 Subject: [PATCH 09/18] vibration sensitivity Add command to set vibration sensitivity of vibration sensor --- miio/gateway/devices/__init__.py | 1 + miio/gateway/devices/sensor.py | 15 +++++++++++++++ miio/gateway/devices/subdevices.yaml | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 miio/gateway/devices/sensor.py diff --git a/miio/gateway/devices/__init__.py b/miio/gateway/devices/__init__.py index 24b3b6bec..093c98224 100644 --- a/miio/gateway/devices/__init__.py +++ b/miio/gateway/devices/__init__.py @@ -3,5 +3,6 @@ # flake8: noqa from .light import LightBulb from .switch import Switch +from .sensor import Vibration from .subdevice import SubDevice, SubDeviceInfo # isort:skip diff --git a/miio/gateway/devices/sensor.py b/miio/gateway/devices/sensor.py new file mode 100644 index 000000000..cdb61fa4f --- /dev/null +++ b/miio/gateway/devices/sensor.py @@ -0,0 +1,15 @@ +"""Xiaomi Zigbee sensors.""" + +import click + +from ...click_common import command +from .subdevice import SubDevice + + +class Vibration(SubDevice): + """Base class for subdevice vibration sensor.""" + + @command(click.argument("vibration_level", type=int)) + def set_vibration_sensitivity(self, vibration_level): + """Set the sensitivity of the vibration sensor, low = 21, medium = 11, high = 1.""" + return self.set_property("vibration_level", vibration_level).pop() diff --git a/miio/gateway/devices/subdevices.yaml b/miio/gateway/devices/subdevices.yaml index 107f9f09f..8288649d4 100644 --- a/miio/gateway/devices/subdevices.yaml +++ b/miio/gateway/devices/subdevices.yaml @@ -337,7 +337,7 @@ type_id: 56 name: Vibration sensor type: AqaraVibration - class: SubDevice + class: Vibration # Thermostats - zigbee_id: lumi.airrtc.tcpecn02 From 2970a944d787b1060ed96d8e7307ad76786130b1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 4 Feb 2021 13:54:27 +0100 Subject: [PATCH 10/18] fix issort --- miio/gateway/devices/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/gateway/devices/__init__.py b/miio/gateway/devices/__init__.py index 093c98224..6bd7f43e0 100644 --- a/miio/gateway/devices/__init__.py +++ b/miio/gateway/devices/__init__.py @@ -2,7 +2,7 @@ # flake8: noqa from .light import LightBulb -from .switch import Switch from .sensor import Vibration +from .switch import Switch from .subdevice import SubDevice, SubDeviceInfo # isort:skip From 01a3aa4413a0ae64900a075cda2d032f010ea356 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 7 Feb 2021 21:15:46 +0100 Subject: [PATCH 11/18] simplify subdevice_model_map --- miio/gateway/gateway.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index 58228d326..275159694 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -94,16 +94,8 @@ def __init__( self._info = None self._subdevice_model_map = None - def _get_subdevice_model_map(self): - if self._subdevice_model_map is None: - filedata = open(os.path.dirname(__file__) + "/devices/subdevices.yaml", "r") - self._subdevice_model_map = yaml.safe_load(filedata) - return self._subdevice_model_map - def _get_unknown_model(self): - self._get_subdevice_model_map() - - for model_info in self._subdevice_model_map: + for model_info in self.subdevice_model_map: if model_info.get("type_id") == -1: return model_info @@ -141,6 +133,15 @@ def model(self): self._info = self.info() return self._info.model + @property + def subdevice_model_map(self): + """Return the subdevice model map.""" + # Check if catch already has the subdevice_model_map, otherwise read it. + if self._subdevice_model_map is None: + filedata = open(os.path.dirname(__file__) + "/devices/subdevices.yaml", "r") + self._subdevice_model_map = yaml.safe_load(filedata) + return self._subdevice_model_map + @command() def discover_devices(self): """ @@ -197,9 +198,7 @@ def match_zigbee_model(self, zigbee_model, sid): Match the zigbee_model to obtain the model_info """ - self._get_subdevice_model_map() - - for model_info in self._subdevice_model_map: + for model_info in self.subdevice_model_map: if model_info.get("zigbee_id") == zigbee_model: return model_info @@ -218,9 +217,7 @@ def match_type_id(self, type_id, sid): Match the type_id to obtain the model_info """ - self._get_subdevice_model_map() - - for model_info in self._subdevice_model_map: + for model_info in self.subdevice_model_map: if model_info.get("type_id") == type_id: return model_info From f2fa7cda5a8aeeb4b493ec02dfca8aa2522c4cca Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 7 Feb 2021 21:46:57 +0100 Subject: [PATCH 12/18] remove unnesesarry comment --- miio/gateway/gateway.py | 1 - 1 file changed, 1 deletion(-) diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index 275159694..3cd00fabf 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -136,7 +136,6 @@ def model(self): @property def subdevice_model_map(self): """Return the subdevice model map.""" - # Check if catch already has the subdevice_model_map, otherwise read it. if self._subdevice_model_map is None: filedata = open(os.path.dirname(__file__) + "/devices/subdevices.yaml", "r") self._subdevice_model_map = yaml.safe_load(filedata) From 4948dce3c9b71f2f0886d50ad48220ee64bc61c2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 7 Feb 2021 21:52:05 +0100 Subject: [PATCH 13/18] move imports to the top --- miio/gateway/devices/subdevice.py | 4 ++-- miio/gateway/gateway.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/miio/gateway/devices/subdevice.py b/miio/gateway/devices/subdevice.py index 0d380aeb5..7d755147b 100644 --- a/miio/gateway/devices/subdevice.py +++ b/miio/gateway/devices/subdevice.py @@ -7,7 +7,7 @@ import click from ...click_common import command -from ..gateway import GATEWAY_MODEL_EU, GATEWAY_MODEL_ZIG3, Gateway, GatewayException +from ..gateway import GATEWAY_MODEL_EU, GATEWAY_MODEL_ZIG3, GatewayException _LOGGER = logging.getLogger(__name__) @@ -31,7 +31,7 @@ class SubDevice: def __init__( self, - gw: Gateway = None, + gw: "Gateway" = None, dev_info: SubDeviceInfo = None, model_info: dict = {}, ) -> None: diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index 3cd00fabf..4ca8731fc 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -26,6 +26,7 @@ class GatewayException(DeviceException): """Exception for the Xioami Gateway communication.""" +from .devices import SubDevice, SubDeviceInfo class Gateway(Device): """Main class representing the Xiaomi Gateway. @@ -148,8 +149,6 @@ def discover_devices(self): and returns a list of the discovered devices. """ - from .devices import SubDeviceInfo - self._devices = {} # Skip the models which do not support getting the device list @@ -235,8 +234,6 @@ def setup_device(self, dev_info, model_info): Setup a device using the SubDeviceInfo and model_info """ - from .devices import SubDevice - if model_info.get("type") == "Gateway": # ignore the gateway itself return From 2e6f4fef85e7e5d2cb73c2952919a2fe53333917 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 7 Feb 2021 21:55:38 +0100 Subject: [PATCH 14/18] fix black --- miio/gateway/gateway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index 4ca8731fc..0bc5662e9 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -26,8 +26,10 @@ class GatewayException(DeviceException): """Exception for the Xioami Gateway communication.""" + from .devices import SubDevice, SubDeviceInfo + class Gateway(Device): """Main class representing the Xiaomi Gateway. From c25636fe7c683a45d615f5e485c31abb4181acb4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 7 Feb 2021 22:05:48 +0100 Subject: [PATCH 15/18] Update miio/gateway/devices/subdevice.py Co-authored-by: Teemu R. --- miio/gateway/devices/subdevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/gateway/devices/subdevice.py b/miio/gateway/devices/subdevice.py index 7d755147b..85e1f69a7 100644 --- a/miio/gateway/devices/subdevice.py +++ b/miio/gateway/devices/subdevice.py @@ -1,7 +1,7 @@ """Xiaomi Gateway subdevice base class.""" import logging -from typing import Optional +from typing import Optional, TYPE_CHECKING import attr import click From 3640d4916c2f99fe2cc9df4b669e217eba6d601d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 7 Feb 2021 22:05:53 +0100 Subject: [PATCH 16/18] Update miio/gateway/devices/subdevice.py Co-authored-by: Teemu R. --- miio/gateway/devices/subdevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/gateway/devices/subdevice.py b/miio/gateway/devices/subdevice.py index 85e1f69a7..f30783e80 100644 --- a/miio/gateway/devices/subdevice.py +++ b/miio/gateway/devices/subdevice.py @@ -10,8 +10,8 @@ from ..gateway import GATEWAY_MODEL_EU, GATEWAY_MODEL_ZIG3, GatewayException _LOGGER = logging.getLogger(__name__) - - +if TYPE_CHECKING: + from ..gateway import Gateway @attr.s(auto_attribs=True) class SubDeviceInfo: """SubDevice discovery info.""" From 64cbb562bf639162a0c9a3d06a96c4b6dbf534ed Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 7 Feb 2021 22:12:50 +0100 Subject: [PATCH 17/18] fix black --- miio/gateway/devices/subdevice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/miio/gateway/devices/subdevice.py b/miio/gateway/devices/subdevice.py index f30783e80..ea7d7a247 100644 --- a/miio/gateway/devices/subdevice.py +++ b/miio/gateway/devices/subdevice.py @@ -1,7 +1,7 @@ """Xiaomi Gateway subdevice base class.""" import logging -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import attr import click @@ -12,6 +12,8 @@ _LOGGER = logging.getLogger(__name__) if TYPE_CHECKING: from ..gateway import Gateway + + @attr.s(auto_attribs=True) class SubDeviceInfo: """SubDevice discovery info.""" @@ -24,10 +26,8 @@ class SubDeviceInfo: class SubDevice: - """ - Base class for all subdevices of the gateway - these devices are connected through zigbee. - """ + """Base class for all subdevices of the gateway these devices are connected through + zigbee.""" def __init__( self, From ec5cdeead965ea4d5d2baeae8704aa956e0e8aa2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 7 Feb 2021 22:17:03 +0100 Subject: [PATCH 18/18] fix isort and flake8 wanting to move above GatewayException class --- miio/gateway/gateway.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/miio/gateway/gateway.py b/miio/gateway/gateway.py index 0bc5662e9..65e69e28d 100644 --- a/miio/gateway/gateway.py +++ b/miio/gateway/gateway.py @@ -27,7 +27,7 @@ class GatewayException(DeviceException): """Exception for the Xioami Gateway communication.""" -from .devices import SubDevice, SubDeviceInfo +from .devices import SubDevice, SubDeviceInfo # noqa: E402 isort:skip class Gateway(Device): @@ -70,7 +70,8 @@ class Gateway(Device): * get_device_prop_exp [[sid, list, of, properties]] ## scene - * get_lumi_bind ["scene", ] for rooms/devices""" + * get_lumi_bind ["scene", ] for rooms/devices + """ def __init__( self, @@ -146,10 +147,7 @@ def subdevice_model_map(self): @command() def discover_devices(self): - """ - Discovers SubDevices - and returns a list of the discovered devices. - """ + """Discovers SubDevices and returns a list of the discovered devices.""" self._devices = {} @@ -194,9 +192,7 @@ def discover_devices(self): @command(click.argument("zigbee_model", "sid")) def match_zigbee_model(self, zigbee_model, sid): - """ - Match the zigbee_model to obtain the model_info - """ + """Match the zigbee_model to obtain the model_info.""" for model_info in self.subdevice_model_map: if model_info.get("zigbee_id") == zigbee_model: @@ -213,9 +209,7 @@ def match_zigbee_model(self, zigbee_model, sid): @command(click.argument("type_id", "sid")) def match_type_id(self, type_id, sid): - """ - Match the type_id to obtain the model_info - """ + """Match the type_id to obtain the model_info.""" for model_info in self.subdevice_model_map: if model_info.get("type_id") == type_id: @@ -232,9 +226,7 @@ def match_type_id(self, type_id, sid): @command(click.argument("dev_info", "model_info")) def setup_device(self, dev_info, model_info): - """ - Setup a device using the SubDeviceInfo and model_info - """ + """Setup a device using the SubDeviceInfo and model_info.""" if model_info.get("type") == "Gateway": # ignore the gateway itself @@ -283,7 +275,7 @@ def set_prop(self, property, value): @command() def clock(self): - """Alarm clock""" + """Alarm clock.""" # payload of clock volume ("get_clock_volume") # already in get_clock response return self.send("get_clock") @@ -304,7 +296,8 @@ def set_developer_key(self, key): @command() def enable_telnet(self): - """Enable root telnet acces to the operating system, use login "admin" or "app", no password.""" + """Enable root telnet acces to the operating system, use login "admin" or "app", + no password.""" try: return self.send("enable_telnet_service") except DeviceError: @@ -321,7 +314,10 @@ def timezone(self): @command() def get_illumination(self): - """Get illumination. In lux?""" + """Get illumination. + + In lux? + """ try: return self.send("get_illumination").pop() except Exception as ex: