Skip to content

Commit

Permalink
Simplified color handling (v5.0.0) (#103)
Browse files Browse the repository at this point in the history
Following the recent firmware update, the gateway now returns more information around colors. This is an attempt to simplify returning what is supported by a bulb. I'm having to go for a breaking change as I need to move methods around in between classes. The color values that are returned seem to be hue, saturation, x and y.

If the user wants to work in the RGB color space, it will have to do the conversion itself. Hence the color example file.

Change log:

color.py:
- Added hex to rgb converter
- Added method that returns what features are supported by a bulb (dimmer, hex, xy, mireds)
- Removed abundant methods

const.py
- Found a set of new constants and added them
- ATTR_LIGHT_COLOR (5706) is hex so I renamed the constant
- Reshuffled constants from other files

device.py
- Removed inferred methods
- Removed RGB color conversion alltogether
- Only return attributes that are relevant for the bulb (ie a white spectrum bulb does return a color temperature, not a rgb value)
- Reordered setters and getters so that they are in the same order in the different classes

Tests
- Moved the test bulbs into a separate file and also added motion sensor and dimmer button devices.
Other:
- Updated tests
  • Loading branch information
Patrik authored Nov 18, 2017
1 parent 4ba3f79 commit 26f66df
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 545 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

**NB:** Latest Gateway version tested and working - 1.2.42.

Python class to communicate with the [IKEA Trådfri](http://www.ikea.com/us/en/catalog/products/00337813/) (Tradfri) ZigBee-based Gateway. Using this library you can, by communicating with the gateway, control IKEA lights (including the RGB ones) and also Philips Hue bulbs. Some of the features include:
Python class to communicate with the [IKEA Trådfri](http://www.ikea.com/us/en/catalog/products/00337813/) (Tradfri) ZigBee-based Gateway. Using this library you can, by communicating with the gateway, control IKEA lights (including the RGB ones). Some of the features include:

- Get information on the gateway
- Observe lights, groups and other resources and get notified when they change
Expand Down Expand Up @@ -67,7 +67,7 @@ api(lights[0].observe(change_listener))
```

## 3. Implement in your own Python platform
Please see the files, example_sync.py, or example_async.py.
Please see the example files.

## 4. Docker support

Expand Down
75 changes: 75 additions & 0 deletions example_color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""
This is a file to give examples on how to work with colors
The gateway supports _some_ hex values, otherwise colors stored as XY
A guess is that IKEA uses the CIE XYZ space
You need to install colormath from pypi in order to this example:
% pip install colormath
Alternatively, as 2.2 of colormath hasn't been released to pypi yet
% pip install git+git://github.com/gtaylor/python-colormath
The gateway returns:
Hue (a guess)
Saturation (a guess)
Brignthess
X
Y
Hex (for some colors)
"""

import sys

from pytradfri import Gateway
from pytradfri.api.libcoap_api import APIFactory

from colormath.color_conversions import convert_color
from colormath.color_objects import sRGBColor, XYZColor


def run():
# Assign configuration variables.
# The configuration check takes care they are present.
api_factory = APIFactory(sys.argv[1])
with open('gateway_psk.txt', 'a+') as file:
file.seek(0)
psk = file.read()
if psk:
api_factory.psk = psk.strip()
else:
psk = api_factory.generate_psk(sys.argv[2])
print('Generated PSK: ', psk)
file.write(psk)
api = api_factory.request

gateway = Gateway()

devices_command = gateway.get_devices()
devices_commands = api(devices_command)
devices = api(devices_commands)
lights = [dev for dev in devices if dev.has_light_control]

rgb = (0, 0, 102)

# Convert RGB to XYZ using a D50 illuminant.
xyz = convert_color(sRGBColor(rgb[0], rgb[1], rgb[2]), XYZColor,
observer='2', target_illuminant='d65')
xy = int(xyz.xyz_x), int(xyz.xyz_y)

# Assuming lights[3] is a RGB bulb
api(lights[3].light_control.set_xy_color(xy[0], xy[1]))

# Assuming lights[3] is a RGB bulb
xy = lights[3].light_control.lights[0].xy_color

# Normalize Z
Z = int(lights[3].light_control.lights[0].dimmer/254*65535)
xyZ = xy+(Z,)
rgb = convert_color(XYZColor(xyZ[0], xyZ[1], xyZ[2]), sRGBColor,
observer='2', target_illuminant='d65')
rgb = (int(rgb.rgb_r), int(rgb.rgb_g), int(rgb.rgb_b))
print(rgb)


run()
164 changes: 33 additions & 131 deletions pytradfri/color.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from .const import (ATTR_LIGHT_COLOR_X as X, ATTR_LIGHT_COLOR_Y as Y)


# Kelvin range for which the conversion functions work
# and that RGB bulbs can show
MIN_KELVIN = 1667
MAX_KELVIN = 25000

# Kelvin range that white-spectrum bulbs can actually show
MIN_KELVIN_WS = 2200
MAX_KELVIN_WS = 4000
from .const import (
ATTR_LIGHT_COLOR_HEX,
ATTR_LIGHT_COLOR_X as X,
ATTR_LIGHT_COLOR_Y as Y,
ATTR_LIGHT_COLOR_SATURATION,
ATTR_LIGHT_COLOR_HUE,
ATTR_LIGHT_DIMMER,
ATTR_LIGHT_MIREDS,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP,
SUPPORT_HEX_COLOR,
SUPPORT_RGB_COLOR,
SUPPORT_XY_COLOR)

# Extracted from Tradfri Android App string.xml
COLOR_NAMES = {
Expand Down Expand Up @@ -39,129 +41,29 @@
for hex, name in COLOR_NAMES.items()}


def can_kelvin_to_xy(k):
return MIN_KELVIN <= k <= MAX_KELVIN


# Only used locally to perform normalization of x, y values
# Scaling to 65535 range and rounding
def normalize_xy(x, y):
return (int(x*65535+0.5), int(y*65535+0.5))


def kelvin_to_xyY(T, white_spectrum_bulb=False):
# Sources: "Design of Advanced Color - Temperature Control System
# for HDTV Applications" [Lee, Cho, Kim]
# and https://en.wikipedia.org/wiki/Planckian_locus#Approximation
# and http://fcam.garage.maemo.org/apiDocs/_color_8cpp_source.html

# Check for Kelvin range for which this function works
if not (MIN_KELVIN <= T <= MAX_KELVIN):
raise ValueError('Kelvin needs to be between {} and {}'.format(
MIN_KELVIN, MAX_KELVIN))

# Check for White-Spectrum kelvin range
if white_spectrum_bulb and not (MIN_KELVIN_WS <= T <= MAX_KELVIN_WS):
raise ValueError('Kelvin needs to be between {} and {} for '
'white spectrum bulbs'.format(
MIN_KELVIN_WS, MAX_KELVIN_WS))

if T <= 4000:
# One number differs on Wikipedia and the paper:
# 0.2343589 is 0.2343580 on Wikipedia... don't know why
x = -0.2661239*(10**9)/T**3 - 0.2343589*(10**6)/T**2 \
+ 0.8776956*(10**3)/T + 0.17991
elif T <= 25000:
x = -3.0258469*(10**9)/T**3 + 2.1070379*(10**6)/T**2 \
+ 0.2226347*(10**3)/T + 0.24039

if T <= 2222:
y = -1.1063814*x**3 - 1.3481102*x**2 + 2.18555832*x - 0.20219683
elif T <= 4000:
y = -0.9549476*x**3 - 1.37418593*x**2 + 2.09137015*x - 0.16748867
elif T <= 25000:
y = 3.081758*x**3 - 5.8733867*x**2 + 3.75112997*x - 0.37001483

x, y = normalize_xy(x, y)
return {X: x, Y: y}


def xyY_to_kelvin(x, y):
# This is an approximation, for information, see the source.
# Source: https://en.wikipedia.org/wiki/Color_temperature#Approximation
# Input range for x and y is 0-65535
n = (x/65535-0.3320) / (y/65535-0.1858)
kelvin = int((-449*n**3 + 3525*n**2 - 6823.3*n + 5520.33) + 0.5)
return kelvin


def rgb2xyzA(r, g, b):
# Uses CIE standard illuminant A = 2856K
# src: http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
# calculation https://gist.github.com/r41d/43e14df2ccaeca56d32796efd6584b48
X = 0.76103282*r + 0.29537849*g + 0.04208869*b
Y = 0.39240755*r + 0.59075697*g + 0.01683548*b
Z = 0.03567341*r + 0.0984595*g + 0.22166709*b
return X, Y, Z


def rgb2xyzD65(r, g, b):
# Uses CIE standard illuminant D65 = 6504K
# src: http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
X = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b
Y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b
Z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b
return X, Y, Z

def supported_features(data):
SUPPORTED_COLOR_FEATURES = 0

def xyz2xyY(X, Y, Z):
total = X + Y + Z
return (0, 0) if total == 0 else normalize_xy(X / total, Y / total)
if ATTR_LIGHT_DIMMER in data:
SUPPORTED_COLOR_FEATURES = SUPPORTED_COLOR_FEATURES\
+ SUPPORT_BRIGHTNESS

if ATTR_LIGHT_COLOR_HEX in data:
SUPPORTED_COLOR_FEATURES = SUPPORTED_COLOR_FEATURES\
+ SUPPORT_HEX_COLOR

def rgb_to_xyY(r, g, b):
# According to http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
# and http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_xyY.html
def prepare(val):
val = max(min(val, 255), 0) / 255.0
if val <= 0.04045:
return val / 12.92
else:
return ((val + 0.055) / 1.055) ** 2.4
r, g, b = map(prepare, (r, g, b))
if ATTR_LIGHT_MIREDS in data:
SUPPORTED_COLOR_FEATURES = SUPPORTED_COLOR_FEATURES\
+ SUPPORT_COLOR_TEMP

x, y = xyz2xyY(*rgb2xyzA(r, g, b))
return {X: x, Y: y}
if X in data and Y in data:
SUPPORTED_COLOR_FEATURES = SUPPORTED_COLOR_FEATURES\
+ SUPPORT_XY_COLOR

if ATTR_LIGHT_MIREDS not in data and X in data and Y in data and \
ATTR_LIGHT_COLOR_SATURATION in data and ATTR_LIGHT_COLOR_HUE\
in data:
SUPPORTED_COLOR_FEATURES = SUPPORTED_COLOR_FEATURES\
+ SUPPORT_RGB_COLOR

# Converted to Python from Obj-C, original source from:
# http://www.developers.meethue.com/documentation/color-conversions-rgb-xy
# pylint: disable=invalid-sequence-index
def xy_brightness_to_rgb(vX: float, vY: float, ibrightness: int):
"""Convert from XYZ to RGB."""
brightness = ibrightness / 255.
if brightness == 0:
return (0, 0, 0)
Y = brightness
if vY == 0:
vY += 0.00000000001
X = (Y / vY) * vX
Z = (Y / vY) * (1 - vX - vY)
# Convert to RGB using Wide RGB D65 conversion.
r = X * 1.656492 - Y * 0.354851 - Z * 0.255038
g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152
b = X * 0.051713 - Y * 0.121364 + Z * 1.011530
# Apply reverse gamma correction.
r, g, b = map(
lambda x: (12.92 * x) if (x <= 0.0031308) else
((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055),
[r, g, b]
)
# Bring all negative components to zero.
r, g, b = map(lambda x: max(0, x), [r, g, b])
# If one component is greater than 1, weight components by that value.
max_component = max(r, g, b)
if max_component > 1:
r, g, b = map(lambda x: x / max_component, [r, g, b])
ir, ig, ib = map(lambda x: int(x * 255), [r, g, b])
return (ir, ig, ib)
return SUPPORTED_COLOR_FEATURES
70 changes: 60 additions & 10 deletions pytradfri/const.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,73 @@
ROOT_DEVICES = "15001"
ROOT_GATEWAY = "15011"
ROOT_GROUPS = "15004"
ROOT_MOODS = "15005"
ROOT_SMART_TASKS = "15010"
ROOT_START_ACTION = "15013" # found under ATTR_START_ACTION
ROOT_GATEWAY = "15011"
ROOT_SWITCH = "15009"

ATTR_AUTH = "9063"
ATTR_PSK = "9091"
ATTR_IDENTITY = "9090"

ATTR_APPLICATION_TYPE = "5750"
ATTR_DEVICE_INFO = "3"
ATTR_NAME = "9001"

ATTR_CREATED_AT = "9002"

ATTR_COMMISSIONING_MODE = "9061"

ATTR_CURRENT_TIME_UNIX = "9059"
ATTR_CURRENT_TIME_ISO8601 = "9060"

ATTR_DEVICE_INFO = "3"

ATTR_GATEWAY_TIME_SOURCE = "9071"
ATTR_GATEWAY_UPDATE_PROGRESS = "9055"

ATTR_HOMEKIT_ID = "9083"

ATTR_ID = "9003"
ATTR_REACHABLE_STATE = "9019"
ATTR_LAST_SEEN = "9020"
ATTR_LIGHT_CONTROL = "3311" # array

ATTR_NAME = "9001"
ATTR_NTP = "9023"
ATTR_FIRMWARE_VERSION = "9029"
ATTR_CURRENT_TIME_UNIX = "9059"
ATTR_CURRENT_TIME_ISO8601 = "9060"
ATTR_FIRST_SETUP = "9069" # ??? unix epoch value when gateway first setup

ATTR_GATEWAY_INFO = "15012"
ATTR_GATEWAY_ID = "9081" # ??? id of the gateway
ATTR_GATEWAY_REBOOT = "9030" # gw reboot
ATTR_GATEWAY_FACTORY_DEFAULTS = "9031" # gw to factory defaults
ATTR_COMMISSIONING_MODE = "9061" # see set_commissioning_timeout
ATTR_GATEWAY_FACTORY_DEFAULTS_MIN_MAX_MSR = "5605"

ATTR_LIGHT_STATE = "5850" # 0 / 1
ATTR_LIGHT_DIMMER = "5851" # Dimmer, not following spec: 0..255
ATTR_LIGHT_COLOR = "5706" # string representing a value in some color space
ATTR_LIGHT_COLOR_HEX = "5706" # string representing a value in hex
ATTR_LIGHT_COLOR_X = "5709"
ATTR_LIGHT_COLOR_Y = "5710"
ATTR_LIGHT_COLOR_SATURATION = "5707" # this is a guess
ATTR_LIGHT_COLOR_HUE = "5708" # this is a guess
ATTR_LIGHT_MIREDS = "5711"

ATTR_MASTER_TOKEN_TAG = "9036"

ATTR_OTA_TYPE = "9066"
ATTR_OTA_UPDATE_STATE = "9054"
ATTR_OTA_UPDATE = "9037"

ATTR_REACHABLE_STATE = "9019"

ATTR_REPEAT_DAYS = "9041"

ATTR_SENSOR = "3300"
ATTR_SENSOR_MAX_RANGE_VALUE = "5604"
ATTR_SENSOR_MAX_MEASURED_VALUE = "5602"
ATTR_SENSOR_MIN_RANGE_VALUE = "5603"
ATTR_SENSOR_MIN_MEASURED_VALUE = "5601"
ATTR_SENSOR_TYPE = "5751"
ATTR_SENSOR_UNIT = "5701"
ATTR_SENSOR_VALUE = "5700"

ATTR_START_ACTION = "9042" # array

Expand All @@ -46,7 +80,23 @@
ATTR_SMART_TASK_TRIGGER_TIME_START_HOUR = "9046"
ATTR_SMART_TASK_TRIGGER_TIME_START_MIN = "9047"

ATTR_SWITCH_PLUG = "3312"
ATTR_SWITCH_POWER_FACTOR = "5820"

ATTR_TRANSITION_TIME = "5712"
ATTR_REPEAT_DAYS = "9041"

ATTR_HOMEKIT_ID = "9083"

# Mireds range for which the conversion functions work
# and that RGB bulbs can show
MIN_MIREDS = 40
MAX_MIREDS = 600

# Mireds range that white-spectrum bulbs can actually show
MIN_MIREDS_WS = 250
MAX_MIREDS_WS = 454

SUPPORT_BRIGHTNESS = 1
SUPPORT_COLOR_TEMP = 2
SUPPORT_HEX_COLOR = 4
SUPPORT_RGB_COLOR = 8
SUPPORT_XY_COLOR = 16
Loading

0 comments on commit 26f66df

Please sign in to comment.