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

Add philips.light.hbulb support #587

Merged
merged 2 commits into from
Dec 4, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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 @@ -113,6 +114,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)