From bf4c7dd81d9a806472935578f3915d3805e6e865 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 14 Aug 2017 08:02:01 +0200 Subject: [PATCH] Pull request https://github.com/rytilahti/python-mirobo/pull/35 merged for testing. --- mirobo/__init__.py | 3 +- mirobo/ceil.py | 112 +++++++++++++++++++++++++++ mirobo/ceil_cli.py | 184 +++++++++++++++++++++++++++++++++++++++++++++ mirobo/protocol.py | 4 +- setup.py | 1 + 5 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 mirobo/ceil.py create mode 100644 mirobo/ceil_cli.py diff --git a/mirobo/__init__.py b/mirobo/__init__.py index b5579ab53..c797c35d7 100644 --- a/mirobo/__init__.py +++ b/mirobo/__init__.py @@ -1,7 +1,8 @@ # flake8: noqa from mirobo.protocol import Message, Utils -from mirobo.vacuumcontainers import VacuumStatus, ConsumableStatus, CleaningDetails, CleaningSummary, Timer +from mirobo.containers import VacuumStatus, ConsumableStatus, CleaningDetails, CleaningSummary, Timer from mirobo.vacuum import Vacuum, VacuumException from mirobo.plug import Plug from mirobo.strip import Strip +from mirobo.ceil import Ceil from mirobo.device import Device, DeviceException diff --git a/mirobo/ceil.py b/mirobo/ceil.py new file mode 100644 index 000000000..e9cc1416a --- /dev/null +++ b/mirobo/ceil.py @@ -0,0 +1,112 @@ +from .device import Device +from typing import Any, Dict + + +class Ceil(Device): + """Main class representing Xiaomi Philips LED Ceiling Lamp.""" + + # TODO: - Auto On/Off Not Supported + # - Adjust Scens with Wall Switch Not Supported + + def on(self): + """Power on.""" + return self.send("set_power", ["on"]) + + def off(self): + """Power off.""" + return self.send("set_power", ["off"]) + + def set_bright(self, level: int): + """Set brightness level.""" + return self.send("set_bright", [level]) + + def set_cct(self, level: int): + """Set Correlated Color Temperature.""" + return self.send("set_cct", [level]) + + def delay_off(self, seconds: int): + """Set delay off seconds.""" + return self.send("delay_off", [seconds]) + + def set_scene(self, num: int): + """Set scene number.""" + return self.send("apply_fixed_scene", [num]) + + def bl_on(self): + """Smart Midnight Light On.""" + return self.send("enable_bl", [1]) + + def bl_off(self): + """Smart Midnight Light off.""" + return self.send("enable_bl", [0]) + + def ac_on(self): + """Auto CCT On.""" + return self.send("enable_ac", [1]) + + def ac_off(self): + """Auto CCT Off.""" + return self.send("enable_ac", [0]) + + def status(self): + """Retrieve properties.""" + properties = ['power', 'bright', 'snm', 'dv', 'cct' + 'sw', 'bl', 'mb', 'ac', 'ms', ] + values = self.send( + "get_prop", + properties + ) + return CeilStatus(dict(zip(properties, values))) + + +class CeilStatus: + """Container for status reports from Xiaomi Philips LED Ceiling Lamp""" + + def __init__(self, data: Dict[str, Any]) -> None: + # ['power', 'bright', 'snm', 'dv', 'cctsw', 'bl', 'mb', 'ac', 'ms'] + # ['off', 0, 4, 0, [[0, 3], [0, 2], [0, 1]], 1, 1, 1] + # NOTE: ms doesn't return any value + self.data = data + + @property + def power(self) -> str: + return self.data["power"] + + @property + def is_on(self) -> bool: + return self.power == "on" + + @property + def bright(self) -> int: + return self.data["bright"] + + @property + def snm(self) -> int: + return self.data["snm"] + + @property + def dv(self) -> int: + return self.data["dv"] + + @property + def cctsw(self) -> tuple: + return self.data["cctsw"] + + @property + def bl(self) -> int: + return self.data["bl"] + + @property + def mb(self) -> int: + return self.data["mb"] + + @property + def ac(self) -> int: + return self.data["ac"] + + def __str__(self) -> str: + s = "" % \ + (self.power, self.bright, self.snm, self.dv, self.cctsw, + self.bl, self.mb, self.ac) + return s diff --git a/mirobo/ceil_cli.py b/mirobo/ceil_cli.py new file mode 100644 index 000000000..0115e37a5 --- /dev/null +++ b/mirobo/ceil_cli.py @@ -0,0 +1,184 @@ +# -*- coding: UTF-8 -*- +import logging +import click +import sys +import ipaddress + +if sys.version_info < (3, 4): + print("To use this script you need python 3.4 or newer, got %s" % + sys.version_info) + sys.exit(1) + +import mirobo # noqa: E402 + +_LOGGER = logging.getLogger(__name__) +pass_dev = click.make_pass_decorator(mirobo.Ceil) + + +def validate_bright(ctx, param, value): + value = int(value) + if value < 1 or value > 100: + raise click.BadParameter('Should be a positive int between 1-100.') + return value + + +def validate_seconds(ctx, param, value): + value = int(value) + if value < 0 or value > 21600: + raise click.BadParameter('Should be a positive int between 1-21600.') + return value + + +def validate_scene(ctx, param, value): + value = int(value) + if value < 1 or value > 4: + raise click.BadParameter('Should be a positive int between 1-4.') + return value + + +def validate_ip(ctx, param, value): + try: + ipaddress.ip_address(value) + return value + except ValueError as ex: + raise click.BadParameter("Invalid IP: %s" % ex) + + +def validate_token(ctx, param, value): + token_len = len(value) + if token_len != 32: + raise click.BadParameter("Token length != 32 chars: %s" % token_len) + return value + + +@click.group(invoke_without_command=True) +@click.option('--ip', envvar="DEVICE_IP", callback=validate_ip) +@click.option('--token', envvar="DEVICE_TOKEN", callback=validate_token) +@click.option('-d', '--debug', default=False, count=True) +@click.pass_context +def cli(ctx, ip: str, token: str, debug: int): + """A tool to command Xiaomi Philips LED Ceiling Lamp.""" + + if debug: + logging.basicConfig(level=logging.DEBUG) + _LOGGER.info("Debug mode active") + else: + logging.basicConfig(level=logging.INFO) + + # if we are scanning, we do not try to connect. + if ctx.invoked_subcommand == "discover": + return + + if ip is None or token is None: + click.echo("You have to give ip and token!") + sys.exit(-1) + + dev = mirobo.Ceil(ip, token, debug) + _LOGGER.debug("Connecting to %s with token %s", ip, token) + + ctx.obj = dev + + if ctx.invoked_subcommand is None: + ctx.invoke(status) + + +@cli.command() +def discover(): + """Search for plugs in the network.""" + mirobo.Ceil.discover() + + +@cli.command() +@pass_dev +def status(dev: mirobo.Ceil): + """Returns the state information.""" + res = dev.status() + if not res: + return # bail out + + click.echo(click.style("Power: %s" % res.power, bold=True)) + click.echo("Brightness: %s" % res.bright) + click.echo("Scene Number: %s" % res.snm) + click.echo("dv: %s" % res.dv) + click.echo("Scenes with Wall Switch: %s" % res.cctsw) + click.echo("Smart Midnight Light: %s" % res.bl) + click.echo("Auto On/Off When Mi Band is nearby: %s" % res.mb) + click.echo("Auto CCT: %s" % res.ac) + + +@cli.command() +@pass_dev +def on(dev: mirobo.Ceil): + """Power on.""" + click.echo("Power on: %s" % dev.on()) + + +@cli.command() +@pass_dev +def off(dev: mirobo.Ceil): + """Power off.""" + click.echo("Power off: %s" % dev.off()) + + +@cli.command() +@click.argument('level', callback=validate_bright, required=True,) +@pass_dev +def set_bright(dev: mirobo.Ceil, level): + """Set brightness level.""" + click.echo("Brightness: %s" % dev.set_bright(level)) + + +@cli.command() +@click.argument('level', callback=validate_bright, required=True,) +@pass_dev +def set_cct(dev: mirobo.Ceil, level): + """Set CCT level.""" + click.echo("CCT level: %s" % dev.set_cct(level)) + + +@cli.command() +@click.argument('seconds', callback=validate_seconds, required=True,) +@pass_dev +def delay_off(dev: mirobo.Ceil, seconds): + """Set delay off in seconds.""" + click.echo("Delay off: %s" % dev.delay_off(seconds)) + + +@cli.command() +@click.argument('scene', callback=validate_scene, required=True,) +@pass_dev +def set_scene(dev: mirobo.Ceil, scene): + """Set scene number.""" + click.echo("Eyecare Scene: %s" % dev.set_scene(scene)) + + +@cli.command() +@pass_dev +def sml_on(dev: mirobo.Ceil): + """Smart Midnight Light on.""" + click.echo("Smart Midnight Light On: %s" % dev.bl_on()) + + +@cli.command() +@pass_dev +def sml_off(dev: mirobo.Ceil): + """Smart Midnight Light off.""" + click.echo("Smart Midnight Light Off: %s" % dev.bl_off()) + + +@cli.command() +@pass_dev +def acct_on(dev: mirobo.Ceil): + """Auto CCT on.""" + click.echo("Auto CCT On: %s" % dev.ac_on()) + + +@cli.command() +@pass_dev +def acct_off(dev: mirobo.Ceil): + """Auto CCT on.""" + click.echo("Auto CCT Off: %s" % dev.ac_off()) + + +if __name__ == "__main__": + cli() diff --git a/mirobo/protocol.py b/mirobo/protocol.py index 161c77153..054d693c5 100644 --- a/mirobo/protocol.py +++ b/mirobo/protocol.py @@ -24,7 +24,8 @@ 0x02f2: "Xiaomi Mi Robot Vacuum", 0x00c4: "Xiaomi Smart Mi Air Purifier", 0x031a: "Xiaomi Smart home gateway", - 0x0330: "Yeelight color bulb" + 0x0330: "Yeelight color bulb", + 0x0374: "Xiaomi Philips LED Ceiling Lamp" } xiaomi_devices = {y: x for x, y in xiaomi_devices_reverse.items()} @@ -128,6 +129,7 @@ def _decode(self, obj, context): jsoned = json.loads(decrypted.decode('utf-8')) except: _LOGGER.error("unable to parse json, was: %s", decrypted) + jsoned = b'{}' raise return jsoned diff --git a/setup.py b/setup.py index 169c61ebb..0be69acc1 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ 'console_scripts': [ 'mirobo=mirobo.vacuum_cli:cli', 'miplug=mirobo.plug_cli:cli', + 'miceil=mirobo.ceil_cli:cli', ], }, )