Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for Xiaomi Philips LED Ceiling Lamp #35

Merged
merged 8 commits into from
Aug 14, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mirobo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
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
112 changes: 112 additions & 0 deletions mirobo/ceil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from .device import Device
from typing import Any, Dict


class Ceil(Device):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

"""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):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be set with typing to return CeilStatus, or is that deliberately left out to avoid moving it on top of this class?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kuduka Could to change this line to def status(self): -> CeilStatus and move the class CeilStatus to the top of the file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed but I think that I did a merge that wasn't needed on this PR, bare with me as I'm not a developer.

If I did something wrong, please let me know so I could improve it :)

"""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)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line at end of file

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line at end of file


class CeilStatus:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

"""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 = "<CeilStatus power=%s, bright=%s, snm=%s, dv=%s, cctsw=%s " \
"bl=%s, mb=%s, ac=%s, >" % \
(self.power, self.bright, self.snm, self.dv, self.cctsw,
self.bl, self.mb, self.ac)
return s
184 changes: 184 additions & 0 deletions mirobo/ceil_cli.py
Original file line number Diff line number Diff line change
@@ -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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

try:
ipaddress.ip_address(value)
return value
except ValueError as ex:
raise click.BadParameter("Invalid IP: %s" % ex)


def validate_token(ctx, param, value):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

def discover():
"""Search for plugs in the network."""
mirobo.Ceil.discover()


@cli.command()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

@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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

@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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

@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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

@pass_dev
def acct_off(dev: mirobo.Ceil):
"""Auto CCT on."""
click.echo("Auto CCT Off: %s" % dev.ac_off())


if __name__ == "__main__":
cli()
3 changes: 2 additions & 1 deletion mirobo/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()}

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'console_scripts': [
'mirobo=mirobo.vacuum_cli:cli',
'miplug=mirobo.plug_cli:cli',
'miceil=mirobo.ceil_cli:cli',
],
},
)