Skip to content

Commit

Permalink
Add basic Philips Moonlight support (Closes: #351) (#359)
Browse files Browse the repository at this point in the history
  • Loading branch information
syssi authored Aug 21, 2018
1 parent 65ee185 commit cc025a5
Show file tree
Hide file tree
Showing 5 changed files with 480 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Supported devices
- :doc:`Xiaomi Philips LED Ceiling Lamp <ceil>` (:class:`miio.ceil`)
- Xiaomi Philips LED Ball Lamp (:class:`miio.philips_bulb`)
- Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp (:class:`miio.philips_bulb`)
- Xiaomi Philips Zhirui Bedroom Smart Lamp (:class:`miio.philips_moonlight`)
- Xiaomi Universal IR Remote Controller (Chuangmi IR) (:class:`miio.chuangmi_ir`)
- Xiaomi Mi Smart Pedestal Fan V2, V3, SA1 and ZA1 (:class:`miio.fan`)
- Xiaomi Mi Air Humidifier (:class:`miio.airhumidifier`)
Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from miio.fan import (Fan, FanV2, FanSA1)
from miio.philips_bulb import PhilipsBulb
from miio.philips_eyecare import PhilipsEyecare
from miio.philips_moonlight import PhilipsMoonlight
from miio.powerstrip import PowerStrip
from miio.protocol import Message, Utils
from miio.vacuum import Vacuum, VacuumException
Expand Down
7 changes: 4 additions & 3 deletions miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import zeroconf

from . import (Device, Vacuum, ChuangmiPlug, PowerStrip, AirPurifier, AirFresh, Ceil,
PhilipsBulb, PhilipsEyecare, ChuangmiIr, AirHumidifier,
WaterPurifier, WifiSpeaker, WifiRepeater, Yeelight, Fan, Cooker,
AirConditioningCompanion)
PhilipsBulb, PhilipsEyecare, PhilipsMoonlight, ChuangmiIr,
AirHumidifier, WaterPurifier, WifiSpeaker, WifiRepeater,
Yeelight, Fan, Cooker, AirConditioningCompanion)

from .chuangmi_plug import (MODEL_CHUANGMI_PLUG_V1, MODEL_CHUANGMI_PLUG_V3,
MODEL_CHUANGMI_PLUG_M1, )
Expand Down Expand Up @@ -51,6 +51,7 @@
"philips-light-ceiling": Ceil,
"philips-light-zyceiling": Ceil,
"philips-light-sread1": PhilipsEyecare, # name needs to be checked
"philips-light-moonlight": PhilipsMoonlight, # name needs to be checked
"xiaomi-wifispeaker-v1": WifiSpeaker, # name needs to be checked
"xiaomi-repeater-v1": WifiRepeater, # name needs to be checked
"xiaomi-repeater-v3": WifiRepeater, # name needs to be checked
Expand Down
251 changes: 251 additions & 0 deletions miio/philips_moonlight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import logging
from collections import defaultdict
from typing import Any, Dict

import click

from .click_common import command, format_output
from .device import Device, DeviceException

_LOGGER = logging.getLogger(__name__)


class PhilipsMoonlightException(DeviceException):
pass


class PhilipsMoonlightStatus:
"""Container for status reports from Xiaomi Philips Zhirui Bedside Lamp."""

def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a Moonlight (philips.light.moonlight):
{'pow': 'off', 'sta': 0, 'bri': 1, 'rgb': 16741971, 'cct': 1, 'snm': 0, 'spr': 0,
'spt': 15, 'wke': 0, 'bl': 1, 'ms': 1, 'mb': 1, 'wkp': [0, 24, 0]}
"""
self.data = data

@property
def power(self) -> str:
return self.data["pow"]

@property
def is_on(self) -> bool:
return self.power == "on"

@property
def brightness(self) -> int:
return self.data["bri"]

@property
def color_temperature(self) -> int:
return self.data["cct"]

@property
def rgb(self) -> int:
return self.data["rgb"]

@property
def scene(self) -> int:
return self.data["snm"]

@property
def sleep_assistant(self) -> int:
"""
Example values:
0: Unknown
1: Unknown
2: Sleep assistant enabled
3: Awake
"""
return self.data["sta"]

@property
def sleep_off_time(self) -> int:
return self.data["spr"]

@property
def total_assistant_sleep_time(self) -> int:
return self.data["spt"]

@property
def brand_sleep(self) -> bool:
# sp_sleep_open?
return self.data["ms"] == 1

@property
def brand(self) -> bool:
# sp_xm_bracelet?
return self.data["mb"] == 1

@property
def wake_up_time(self) -> [int, int, int]:
# Example: [weekdays?, hour, minute]
return self.data["wkp"]

def __repr__(self) -> str:
s = "<PhilipsMoonlightStatus power=%s, " \
"brightness=%s, " \
"color_temperature=%s, " \
"rgb=%s, " \
"scene=%s>" % \
(self.power,
self.brightness,
self.color_temperature,
self.rgb,
self.scene)
return s

def __json__(self):
return self.data


class PhilipsMoonlight(Device):
"""Main class representing Xiaomi Philips Zhirui Bedside Lamp.
Not yet implemented features/methods:
add_mb # Add miband
get_band_period # Bracelet work time
get_mb_rssi # Miband RSSI
get_mb_mac # Miband MAC address
enable_mibs
set_band_period
miIO.bleStartSearchBand
miIO.bleGetNearbyBandList
enable_sub_voice # Sub voice control?
enable_voice # Voice control
skip_breath
set_sleep_time
set_wakeup_time
en_sleep
en_wakeup
go_night # Night light / read mode
get_wakeup_time
enable_bl # Night light
"""

@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Brightness: {result.brightness}\n"
"Color temperature: {result.color_temperature}\n"
"RGB: {result.rgb}\n"
"Scene: {result.scene}\n"
)
)
def status(self) -> PhilipsMoonlightStatus:
"""Retrieve properties."""
properties = ['pow', 'sta', 'bri', 'rgb', 'cct', 'snm', 'spr', 'spt', 'wke', 'bl', 'ms',
'mb', 'wkp']
values = self.send(
"get_prop",
properties
)

properties_count = len(properties)
values_count = len(values)
if properties_count != values_count:
_LOGGER.debug(
"Count (%s) of requested properties does not match the "
"count (%s) of received values.",
properties_count, values_count)

return PhilipsMoonlightStatus(
defaultdict(lambda: None, zip(properties, values)))

@command(
default_output=format_output("Powering on"),
)
def on(self):
"""Power on."""
return self.send("set_power", ["on"])

@command(
default_output=format_output("Powering off"),
)
def off(self):
"""Power off."""
return self.send("set_power", ["off"])

@command(
click.argument("rgb", type=int),
default_output=format_output("Setting color to {rgb}")
)
def set_rgb(self, rgb):
"""Set color in encoded RGB."""
if rgb < 0 or rgb > 16777215:
raise PhilipsMoonlightException("Invalid color: %s" % rgb)

return self.send("set_rgb", [rgb])

@command(
click.argument("level", type=int),
default_output=format_output("Setting brightness to {level}")
)
def set_brightness(self, level: int):
"""Set brightness level."""
if level < 1 or level > 100:
raise PhilipsMoonlightException("Invalid brightness: %s" % level)

return self.send("set_bright", [level])

@command(
click.argument("level", type=int),
default_output=format_output("Setting color temperature to {level}")
)
def set_color_temperature(self, level: int):
"""Set Correlated Color Temperature."""
if level < 1 or level > 100:
raise PhilipsMoonlightException("Invalid color temperature: %s" % level)

return self.send("set_cct", [level])

@command(
click.argument("brightness", type=int),
click.argument("cct", type=int),
default_output=format_output(
"Setting brightness to {brightness} and color temperature to {cct}")
)
def set_brightness_and_color_temperature(self, brightness: int, cct: int):
"""Set brightness level and the correlated color temperature."""
if brightness < 1 or brightness > 100:
raise PhilipsMoonlightException("Invalid brightness: %s" % brightness)

if cct < 1 or cct > 100:
raise PhilipsMoonlightException("Invalid color temperature: %s" % cct)

return self.send("set_bricct", [brightness, cct])

@command(
click.argument("brightness", type=int),
click.argument("rgb", type=int),
default_output=format_output(
"Setting brightness to {brightness} and color to {rgb}")
)
def set_brightness_and_rgb(self, brightness: int, rgb: int):
"""Set brightness level and the color."""
if brightness < 1 or brightness > 100:
raise PhilipsMoonlightException("Invalid brightness: %s" % brightness)

if rgb < 0 or rgb > 16777215:
raise PhilipsMoonlightException("Invalid color: %s" % rgb)

return self.send("set_brirgb", [brightness, rgb])

@command(
click.argument("number", type=int),
default_output=format_output("Setting fixed scene to {number}")
)
def set_scene(self, number: int):
"""Set scene number."""
if number < 1 or number > 4:
raise PhilipsMoonlightException("Invalid fixed scene number: %s" % number)

return self.send("apply_fixed_scene", [number])
Loading

0 comments on commit cc025a5

Please sign in to comment.