Skip to content
This repository has been archived by the owner on Mar 8, 2022. It is now read-only.

Commit

Permalink
Merge pull request #8 from angadsingh/master
Browse files Browse the repository at this point in the history
Bug fixes and improvements
  • Loading branch information
sbidy authored Mar 18, 2020
2 parents f31f229 + caa90ce commit e2c8bb0
Show file tree
Hide file tree
Showing 6 changed files with 532 additions and 72 deletions.
37 changes: 21 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
# wiz_light
A WiZ Light integration for Home Assistant.
A Home assistant integration for Phillips WiZ Light bulbs

This is a fork of https://github.com/sbidy/wiz_light/ and fixes several issues with the component:

Bug fixes:
- Fixes https://github.com/sbidy/wiz_light/issues/6: make the whole component truly async using non-blocking UDP
- Light control now works even when lights are set to a rhythm.

Features:
- Now supports switching the light to rhythm mode! (rhythm is defined as a scene for HA)
- Implements a pattern of sending multiple command UDP datagrams until response is received
- Consolidates getPilot and setPilot calls using a PilotBuilder and PilotParser. Removes unnecessary UDP calls for each and every attribute (color, temperature, brightness, scene, etc.) and makes a combined getPilot/setPilot call
- enhanced debug logging for UDP

This component does not need a dependency on `pywizlight` like @sbidy's component

## Next improvement:
- deterministic selection of following bulb types: Only dimmable, dimmable + white color temprature (kelvin) and full RGB support also with white color temperatur. Maybe with an auto detect feature, try and error testing or via configuration YAML.
- Prepare for hacs.xyz
- Implement hacs.xyz structure

Working features
## Working features
- Brigtness
- Color (RGB)
- White Color Temprature
- On/Off
- On/Off, Toggle
- Effects
- Setting a rhythm as a scene

Next up:
- Some improvements and bugfixes to create a more stable integration
- testing with other hardware -- **Contribution required !!**


## Install for testing
If you want to try the integration please clone this repo to `<confdir>/custom_components/`.

Run `git clone https://github.com/sbidy/wiz_light` within the `<confdir>/custom_components/`.

You also have to install the `pip install pywizlight` package. More infos? Check my git [pywizlight](https://github.com/sbidy/pywizlight)
## Testing
See `test.py` for how the underlying API works

## HA config
To enable the platform integration add
Expand Down
128 changes: 73 additions & 55 deletions light.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

import logging
import voluptuous as vol
from pywizlight import wizlight
from .wizlight import wizlight, PilotBuilder, PilotParser
from .scenes import SCENES
from homeassistant.exceptions import InvalidStateError
from homeassistant.core import callback

Expand All @@ -37,9 +38,10 @@
_LOGGER = logging.getLogger(__name__)

# Validation of the user's configuration

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_NAME): cv.string
})

SUPPORT_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT )
Expand Down Expand Up @@ -114,24 +116,46 @@ async def async_turn_on(self, **kwargs):
"""
Instruct the light to turn on.
"""
# TODO: change this to set state using a single UDP call
#


rgb = None
if ATTR_RGB_COLOR in kwargs:
self._light.rgb = kwargs[ATTR_RGB_COLOR]
rgb = kwargs[ATTR_RGB_COLOR]
if ATTR_HS_COLOR in kwargs:
self._light.rgb = color_utils.color_hs_to_RGB(kwargs[ATTR_HS_COLOR][0], kwargs[ATTR_HS_COLOR][1])
rgb = color_utils.color_hs_to_RGB(kwargs[ATTR_HS_COLOR][0], kwargs[ATTR_HS_COLOR][1])

brightness = None
if ATTR_BRIGHTNESS in kwargs:
self._light.brightness = kwargs[ATTR_BRIGHTNESS]
brightness = kwargs[ATTR_BRIGHTNESS]

colortemp = None
if ATTR_COLOR_TEMP in kwargs:
kelvin = color_utils.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
self._light.colortemp = kelvin
colortemp = kelvin

sceneid = None
if ATTR_EFFECT in kwargs:
self._light.scene = self.scene_helper(kwargs[ATTR_EFFECT])
self._light.turn_on()
sceneid = self._light.get_id_from_scene_name(kwargs[ATTR_EFFECT])

if sceneid == 1000: #rhythm
pilot = PilotBuilder()
else:
pilot = PilotBuilder(
rgb = rgb,
brightness = brightness,
colortemp = colortemp,
scene = sceneid
)

await self._light.turn_on(pilot)

async def async_turn_off(self, **kwargs):
"""
Instruct the light to turn off.
"""
self._light.turn_off()
await self._light.turn_off()

@property
def color_temp(self):
Expand Down Expand Up @@ -181,25 +205,47 @@ async def async_update(self):
Fetch new state data for this light.
This is the only method that should fetch new data for Home Assistant.
"""
self.update_availability()
if self._available is True:
self.update_state()
await self.update_state()

if self._state != None and self._state != False:
self.update_brightness()
self.update_temperature()
self.update_color()
self.update_effect()
self.update_scene_list()

# ---- CALLBACKS -----
@callback
async def update_state_available(self):
self._state = self._light.status
self._available = True

async def update_state_unavailable(self):
self._state = False
self._available = False

async def update_state(self):
"""
Update the state
"""
try:
_LOGGER.debug("[wizlight {}] updating state".format(self._light.ip))
await self._light.updateState()
if self._light.state == None:
await self.update_state_unavailable()
else:
await self.update_state_available()
except Exception as ex:
_LOGGER.error(ex)
await self.update_state_unavailable()
_LOGGER.debug("[wizlight {}] updated state: {}".format(self._light.ip, self._state))

def update_brightness(self):
"""
Update the brightness.
"""
if self._light.brightness is None:
if self._light.state.get_brightness() is None:
return
try:
brightness = self._light.brightness
brightness = self._light.state.get_brightness()
if 0 <= int(brightness) <= 255:
self._brightness = int(brightness)
else:
Expand All @@ -210,41 +256,28 @@ def update_brightness(self):
except Exception as ex:
_LOGGER.error(ex)
self._state = None
@callback
def update_state(self):
"""
Update the state
"""
if self._light.status is None:
return
try:
self._state = self._light.status
return
except Exception as ex:
_LOGGER.error(ex)
self._state = None
@callback

def update_temperature(self):
"""
Update the temperature
"""
if self._light.colortemp is None:
if self._light.state.get_colortemp() is None:
return
try:
temperature = color_utils.color_temperature_kelvin_to_mired(self._light.colortemp)
temperature = color_utils.color_temperature_kelvin_to_mired(self._light.state.get_colortemp())
self._temperature = temperature
except Exception:
_LOGGER.error("Cannot evaluate temperature", exc_info=True)
self._temperature = None
@callback

def update_color(self):
"""
Update the hs color
"""
if self._light.rgb is None:
if self._light.state.get_rgb() is None:
return
try:
r, g, b = self._light.rgb
r, g, b = self._light.state.get_rgb()
if r is None:
# this is the case if the temperature was changed - no infomation was return form the lamp.
# do nothing until the RGB color was changed
Expand All @@ -260,30 +293,15 @@ def update_color(self):
except Exception:
_LOGGER.error("Cannot evaluate color", exc_info=True)
self._hscolor = None

@callback
def update_availability(self):
'''
update the bulb availability
'''
self._available = self._light.getConnection()

@callback
def update_effect(self):
'''
update the bulb availability
update the bulb scene
'''
self._effect = self._light.scene
self._effect = self._light.state.get_scene()

# this should be improved :-)
@callback
# TODO: this should be improved :-)
def update_scene_list(self):
self._scenes = []
for id in self._light.SCENES:
self._scenes.append(self._light.SCENES[id])

# move to pywizlight 0.2.6
def scene_helper(self, scene):
for id in self._light.SCENES:
if self._light.SCENES[id] == scene:
return id
for id in SCENES:
self._scenes.append(SCENES[id])
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"documentation": "https://github.com/sbidy/wiz_light",
"dependencies": [],
"codeowners": ["@sbidy"],
"requirements": ["pywizlight==0.2.5"]
"requirements": ["asyncio_dgram==1.0.1"]
}
35 changes: 35 additions & 0 deletions scenes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
SCENES = {
1:"Ocean",
2:"Romance",
3:"Sunset",
4:"Party",
5:"Fireplace",
6:"Cozy",
7:"Forest",
8:"Pastel Colors",
9:"Wake up",
10:"Bedtime",
11:"Warm White",
12:"Daylight",
13:"Cool white",
14:"Night light",
15:"Focus",
16:"Relax",
17:"True colors",
18:"TV time",
19:"Plantgrowth",
20:"Spring",
21:"Summer",
22:"Fall",
23:"Deepdive",
24:"Jungle",
25:"Mojito",
26:"Club",
27:"Christmas",
28:"Halloween",
29:"Candlelight",
30:"Golden white",
31:"Pulse",
32:"Steampunk",
1000:"Rhythm"
}
72 changes: 72 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import asyncio
import asyncio_dgram
import time
from .wizlight import wizlight, PilotBuilder
import logging
import sys

# python3 -m wiz_light.test

root = logging.getLogger()
root.setLevel(logging.DEBUG)

handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
root.addHandler(handler)

_LOGGER = logging.getLogger(__name__)

async def testbulb(bulb):
wait_secs=10

state = await bulb.updateState()

if state.get_state(): # check if on
await asyncio.wait_for(bulb.turn_off(), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.lightSwitch(), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.turn_off(), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.turn_on(PilotBuilder(brightness = 255)), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.turn_on(PilotBuilder(brightness = 50)), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.turn_on(PilotBuilder(rgb = (50, 100, 200))), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.turn_on(PilotBuilder(colortemp = 4000)), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.turn_on(PilotBuilder(colortemp = 6500)), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.turn_on(PilotBuilder(scene = 14)), wait_secs)
await asyncio.sleep(0.5)
await asyncio.wait_for(bulb.turn_on(PilotBuilder(scene = 24)), wait_secs)

state = await bulb.updateState()
print(state.get_state())
print(state.get_scene())
print(state.get_scene())
print(state.get_warm_white())
print(state.get_speed())
print(state.get_cold_white())
print(state.get_rgb())
print(state.get_brightness())
print(state.get_colortemp())


async def run_bulb_automation():
loop = asyncio.get_event_loop()
bulb1 = wizlight('192.168.1.58')
bulb2 = wizlight('192.168.1.7')
# await asyncio.gather(testbulb(bulb1), testbulb(bulb2), loop = loop)
state = await bulb1.updateState()
await bulb1.turn_on(PilotBuilder(scene = 14))
state = await bulb1.updateState()
await asyncio.sleep(0.5)
await bulb1.turn_on(PilotBuilder()) #rhythm
state = await bulb1.updateState()

if __name__ == '__main__':
asyncio.run(run_bulb_automation())
Loading

0 comments on commit e2c8bb0

Please sign in to comment.