Skip to content

Commit

Permalink
Add philips.light.rwread support (#589)
Browse files Browse the repository at this point in the history
* First draft

* Add tests

* Fix docstring
  • Loading branch information
syssi authored and rytilahti committed Dec 5, 2019
1 parent 72c4324 commit 1a52117
Show file tree
Hide file tree
Showing 5 changed files with 401 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Supported devices
- Xiaomi Chuangmi Plug V3 (1 Socket, 2 USB Ports)
- Xiaomi Smart Power Strip V1 and V2 (WiFi, 6 Ports)
- Xiaomi Philips Eyecare Smart Lamp 2
- Xiaomi Philips RW Read (philips.light.rwread)
- Xiaomi Philips LED Ceiling Lamp
- Xiaomi Philips LED Ball Lamp (philips.light.bulb)
- Xiaomi Philips LED Ball Lamp White (philips.light.hbulb)
Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from miio.philips_bulb import PhilipsBulb, PhilipsWhiteBulb
from miio.philips_eyecare import PhilipsEyecare
from miio.philips_moonlight import PhilipsMoonlight
from miio.philips_rwread import PhilipsRwread
from miio.powerstrip import PowerStrip
from miio.protocol import Message, Utils
from miio.pwzn_relay import PwznRelay
Expand Down
2 changes: 2 additions & 0 deletions miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PhilipsBulb,
PhilipsEyecare,
PhilipsMoonlight,
PhilipsRwread,
PhilipsWhiteBulb,
PowerStrip,
Toiletlid,
Expand Down Expand Up @@ -122,6 +123,7 @@
"philips-light-zyceiling": Ceil,
"philips-light-sread1": PhilipsEyecare, # name needs to be checked
"philips-light-moonlight": PhilipsMoonlight, # name needs to be checked
"philips-light-rwread": PhilipsRwread, # 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
230 changes: 230 additions & 0 deletions miio/philips_rwread.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import enum
import logging
from collections import defaultdict
from typing import Any, Dict

import click

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

_LOGGER = logging.getLogger(__name__)

MODEL_PHILIPS_LIGHT_RWREAD = "philips.light.rwread"

AVAILABLE_PROPERTIES = {
MODEL_PHILIPS_LIGHT_RWREAD: ["power", "bright", "dv", "snm", "flm", "chl", "flmv"],
}


class PhilipsRwreadException(DeviceException):
pass


class MotionDetectionSensitivity(enum.Enum):
Low = 1
Medium = 2
High = 3


class PhilipsRwreadStatus:
"""Container for status reports from Xiaomi Philips RW Read"""

def __init__(self, data: Dict[str, Any]) -> None:
"""
Response of a RW Read (philips.light.rwread):
{'power': 'on', 'bright': 53, 'dv': 0, 'snm': 1,
'flm': 0, 'chl': 0, 'flmv': 0}
"""
self.data = data

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

@property
def is_on(self) -> bool:
"""True if the device is turned on."""
return self.power == "on"

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

@property
def delay_off_countdown(self) -> int:
"""Countdown until turning off in seconds."""
return self.data["dv"]

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

@property
def motion_detection(self) -> bool:
"""True if motion detection is enabled."""
return self.data["flm"] == 1

@property
def motion_detection_sensitivity(self) -> MotionDetectionSensitivity:
"""The sensitivity of the motion detection."""
return MotionDetectionSensitivity(self.data["flmv"])

@property
def child_lock(self) -> bool:
"""True if child lock is enabled."""
return self.data["chl"] == 1

def __repr__(self) -> str:
s = (
"<PhilipsRwreadStatus power=%s, "
"brightness=%s, "
"delay_off_countdown=%s, "
"scene=%s, "
"motion_detection=%s, "
"motion_detection_sensitivity=%s, "
"child_lock=%s>"
% (
self.power,
self.brightness,
self.delay_off_countdown,
self.scene,
self.motion_detection,
self.motion_detection_sensitivity,
self.child_lock,
)
)
return s

def __json__(self):
return self.data


class PhilipsRwread(Device):
"""Main class representing Xiaomi Philips RW Read."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
model: str = MODEL_PHILIPS_LIGHT_RWREAD,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)

if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_PHILIPS_LIGHT_RWREAD

@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Brightness: {result.brightness}\n"
"Delayed turn off: {result.delay_off_countdown}\n"
"Scene: {result.scene}\n"
"Motion detection: {result.motion_detection}\n"
"Motion detection sensitivity: {result.motion_detection_sensitivity}\n"
"Child lock: {result.child_lock}\n",
)
)
def status(self) -> PhilipsRwreadStatus:
"""Retrieve properties."""
properties = AVAILABLE_PROPERTIES[self.model]
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 PhilipsRwreadStatus(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("level", type=int),
default_output=format_output("Setting brightness to {level}"),
)
def set_brightness(self, level: int):
"""Set brightness level of the primary light."""
if level < 1 or level > 100:
raise PhilipsRwreadException("Invalid brightness: %s" % level)

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

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

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

@command(
click.argument("seconds", type=int),
default_output=format_output("Setting delayed turn off to {seconds} seconds"),
)
def delay_off(self, seconds: int):
"""Set delay off in seconds."""

if seconds < 0:
raise PhilipsRwreadException(
"Invalid value for a delayed turn off: %s" % seconds
)

return self.send("delay_off", [seconds])

@command(
click.argument("motion_detection", type=bool),
default_output=format_output(
lambda motion_detection: "Turning on motion detection"
if motion_detection
else "Turning off motion detection"
),
)
def set_motion_detection(self, motion_detection: bool):
"""Set motion detection on/off."""
return self.send("enable_flm", [int(motion_detection)])

@command(
click.argument("sensitivity", type=EnumType(MotionDetectionSensitivity, False)),
default_output=format_output(
"Setting motion detection sensitivity to {sensitivity}"
),
)
def set_motion_detection_sensitivity(self, sensitivity: MotionDetectionSensitivity):
"""Set motion detection sensitivity."""
return self.send("set_flmvalue", [sensitivity.value])

@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock" if lock else "Turning off child lock"
),
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
return self.send("enable_chl", [int(lock)])
Loading

0 comments on commit 1a52117

Please sign in to comment.