Skip to content

Commit

Permalink
Add philips.light.hbulb support (#587)
Browse files Browse the repository at this point in the history
  • Loading branch information
syssi authored Dec 4, 2019
1 parent 57c1009 commit 72c4324
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 33 deletions.
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Supported devices
- Xiaomi Smart Power Strip V1 and V2 (WiFi, 6 Ports)
- Xiaomi Philips Eyecare Smart Lamp 2
- Xiaomi Philips LED Ceiling Lamp
- Xiaomi Philips LED Ball Lamp
- Xiaomi Philips LED Ball Lamp (philips.light.bulb)
- Xiaomi Philips LED Ball Lamp White (philips.light.hbulb)
- Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp
- Xiaomi Philips Zhirui Bedroom Smart Lamp
- Xiaomi Universal IR Remote Controller (Chuangmi IR)
Expand Down
2 changes: 1 addition & 1 deletion miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from miio.cooker import Cooker
from miio.device import Device, DeviceError, DeviceException
from miio.fan import Fan, FanP5, FanSA1, FanV2, FanZA1, FanZA4
from miio.philips_bulb import PhilipsBulb
from miio.philips_bulb import PhilipsBulb, PhilipsWhiteBulb
from miio.philips_eyecare import PhilipsEyecare
from miio.philips_moonlight import PhilipsMoonlight
from miio.powerstrip import PowerStrip
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,
PhilipsWhiteBulb,
PowerStrip,
Toiletlid,
Vacuum,
Expand Down Expand Up @@ -114,6 +115,7 @@
),
"yunmi-waterpuri-v2": WaterPurifier,
"philips-light-bulb": PhilipsBulb, # cannot be discovered via mdns
"philips-light-hbulb": PhilipsWhiteBulb, # cannot be discovered via mdns
"philips-light-candle": PhilipsBulb, # cannot be discovered via mdns
"philips-light-candle2": PhilipsBulb, # cannot be discovered via mdns
"philips-light-ceiling": Ceil,
Expand Down
117 changes: 88 additions & 29 deletions miio/philips_bulb.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from collections import defaultdict
from typing import Any, Dict
from typing import Any, Dict, Optional

import click

Expand All @@ -9,6 +9,19 @@

_LOGGER = logging.getLogger(__name__)

MODEL_PHILIPS_LIGHT_BULB = "philips.light.bulb"
MODEL_PHILIPS_LIGHT_HBULB = "philips.light.hbulb"

AVAILABLE_PROPERTIES_COMMON = [
"power",
"dv",
]

AVAILABLE_PROPERTIES = {
MODEL_PHILIPS_LIGHT_HBULB: AVAILABLE_PROPERTIES_COMMON + ["bri"],
MODEL_PHILIPS_LIGHT_BULB: AVAILABLE_PROPERTIES_COMMON + ["bright", "cct", "snm"],
}


class PhilipsBulbException(DeviceException):
pass
Expand All @@ -30,31 +43,42 @@ def is_on(self) -> bool:
return self.power == "on"

@property
def brightness(self) -> int:
return self.data["bright"]
def brightness(self) -> Optional[int]:
if "bright" in self.data:
return self.data["bright"]
if "bri" in self.data:
return self.data["bri"]
return None

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

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

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

def __repr__(self) -> str:
s = (
"<PhilipsBulbStatus power=%s, brightness=%s, "
"color_temperature=%s, scene=%s, delay_off_countdown=%s>"
"<PhilipsBulbStatus power=%s, "
"brightness=%s, "
"delay_off_countdown=%s, "
"color_temperature=%s, "
"scene=%s>"
% (
self.power,
self.brightness,
self.delay_off_countdown,
self.color_temperature,
self.scene,
self.delay_off_countdown,
)
)
return s
Expand All @@ -63,22 +87,39 @@ def __json__(self):
return self.data


class PhilipsBulb(Device):
"""Main class representing Xiaomi Philips LED Ball Lamp."""
class PhilipsWhiteBulb(Device):
"""Main class representing Xiaomi Philips White LED Ball Lamp."""

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_HBULB,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)

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

@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Brightness: {result.brightness}\n"
"Delayed turn off: {result.delay_off_countdown}\n"
"Color temperature: {result.color_temperature}\n"
"Scene: {result.scene}\n"
"Delayed turn off: {result.delay_off_countdown}\n",
"Scene: {result.scene}\n",
)
)
def status(self) -> PhilipsBulbStatus:
"""Retrieve properties."""
properties = ["power", "bright", "cct", "snm", "dv"]

properties = AVAILABLE_PROPERTIES[self.model]
values = self.send("get_prop", properties)

properties_count = len(properties)
Expand Down Expand Up @@ -114,6 +155,38 @@ def set_brightness(self, level: int):

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

@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 seconds."""

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

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


class PhilipsBulb(PhilipsWhiteBulb):
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_BULB,
) -> None:
if model in AVAILABLE_PROPERTIES:
self.model = model
else:
self.model = MODEL_PHILIPS_LIGHT_BULB

super().__init__(ip, token, start_id, debug, lazy_discover, self.model)

@command(
click.argument("level", type=int),
default_output=format_output("Setting color temperature to {level}"),
Expand Down Expand Up @@ -142,20 +215,6 @@ def set_brightness_and_color_temperature(self, brightness: int, cct: int):

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

@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 seconds."""

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

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

@command(
click.argument("number", type=int),
default_output=format_output("Setting fixed scene to {number}"),
Expand Down
97 changes: 95 additions & 2 deletions miio/tests/test_philips_bulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

import pytest

from miio import PhilipsBulb
from miio.philips_bulb import PhilipsBulbException, PhilipsBulbStatus
from miio import PhilipsBulb, PhilipsWhiteBulb
from miio.philips_bulb import (
MODEL_PHILIPS_LIGHT_BULB,
MODEL_PHILIPS_LIGHT_HBULB,
PhilipsBulbException,
PhilipsBulbStatus,
)

from .dummies import DummyDevice


class DummyPhilipsBulb(DummyDevice, PhilipsBulb):
def __init__(self, *args, **kwargs):
self.model = MODEL_PHILIPS_LIGHT_BULB
self.state = {"power": "on", "bright": 100, "cct": 10, "snm": 0, "dv": 0}
self.return_values = {
"get_prop": self._get_state,
Expand Down Expand Up @@ -170,3 +176,90 @@ def scene():

with pytest.raises(PhilipsBulbException):
self.device.set_scene(5)


class DummyPhilipsWhiteBulb(DummyDevice, PhilipsWhiteBulb):
def __init__(self, *args, **kwargs):
self.model = MODEL_PHILIPS_LIGHT_HBULB
self.state = {"power": "on", "bri": 100, "dv": 0}
self.return_values = {
"get_prop": self._get_state,
"set_power": lambda x: self._set_state("power", x),
"set_bright": lambda x: self._set_state("bri", x),
"delay_off": lambda x: self._set_state("dv", x),
}
super().__init__(args, kwargs)


@pytest.fixture(scope="class")
def philips_white_bulb(request):
request.cls.device = DummyPhilipsWhiteBulb()
# TODO add ability to test on a real device


@pytest.mark.usefixtures("philips_white_bulb")
class TestPhilipsWhiteBulb(TestCase):
def is_on(self):
return self.device.status().is_on

def state(self):
return self.device.status()

def test_on(self):
self.device.off() # ensure off
assert self.is_on() is False

self.device.on()
assert self.is_on() is True

def test_off(self):
self.device.on() # ensure on
assert self.is_on() is True

self.device.off()
assert self.is_on() is False

def test_status(self):
self.device._reset_state()

assert repr(self.state()) == repr(PhilipsBulbStatus(self.device.start_state))

assert self.is_on() is True
assert self.state().brightness == self.device.start_state["bri"]
assert self.state().delay_off_countdown == self.device.start_state["dv"]
assert self.state().color_temperature is None
assert self.state().scene is None

def test_set_brightness(self):
def brightness():
return self.device.status().brightness

self.device.set_brightness(1)
assert brightness() == 1
self.device.set_brightness(50)
assert brightness() == 50
self.device.set_brightness(100)

with pytest.raises(PhilipsBulbException):
self.device.set_brightness(-1)

with pytest.raises(PhilipsBulbException):
self.device.set_brightness(0)

with pytest.raises(PhilipsBulbException):
self.device.set_brightness(101)

def test_delay_off(self):
def delay_off_countdown():
return self.device.status().delay_off_countdown

self.device.delay_off(100)
assert delay_off_countdown() == 100
self.device.delay_off(200)
assert delay_off_countdown() == 200

with pytest.raises(PhilipsBulbException):
self.device.delay_off(-1)

with pytest.raises(PhilipsBulbException):
self.device.delay_off(0)

0 comments on commit 72c4324

Please sign in to comment.