-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
731 additions
and
383 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,14 @@ | ||
# IDE | ||
.idea/* | ||
sf.json | ||
phue-rf-save.json | ||
|
||
# venv | ||
venv | ||
|
||
# cached files | ||
__pycache__/* | ||
external/__pycache__/* | ||
external/modified/__pycache__/* | ||
sims/__pycache__/* | ||
sims/__pycache__/* | ||
|
||
# project-specific files | ||
phue-rf-save.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
# | ||
# BEWARE: THIS IS NOT THE ORIGINAL rgbxy.py FILE FOUND ON https://github.com/benknight/hue-python-rgb-converter | ||
# | ||
# The following changes were made: | ||
# Added if statement to avoid division by zero error | ||
# | ||
# | ||
import math | ||
import random | ||
from collections import namedtuple | ||
|
||
__version__ = '0.5.1' | ||
|
||
# Represents a CIE 1931 XY coordinate pair. | ||
XYPoint = namedtuple('XYPoint', ['x', 'y']) | ||
|
||
# LivingColors Iris, Bloom, Aura, LightStrips | ||
GamutA = ( | ||
XYPoint(0.704, 0.296), | ||
XYPoint(0.2151, 0.7106), | ||
XYPoint(0.138, 0.08), | ||
) | ||
|
||
# Hue A19 bulbs | ||
GamutB = ( | ||
XYPoint(0.675, 0.322), | ||
XYPoint(0.4091, 0.518), | ||
XYPoint(0.167, 0.04), | ||
) | ||
|
||
# Hue BR30, A19 (Gen 3), Hue Go, LightStrips plus | ||
GamutC = ( | ||
XYPoint(0.692, 0.308), | ||
XYPoint(0.17, 0.7), | ||
XYPoint(0.153, 0.048), | ||
) | ||
|
||
|
||
def get_light_gamut(modelId): | ||
"""Gets the correct color gamut for the provided model id. | ||
Docs: https://developers.meethue.com/develop/hue-api/supported-devices/ | ||
""" | ||
if modelId in ('LST001', 'LLC005', 'LLC006', 'LLC007', 'LLC010', 'LLC011', 'LLC012', 'LLC013', 'LLC014'): | ||
return GamutA | ||
elif modelId in ('LCT001', 'LCT007', 'LCT002', 'LCT003', 'LLM001'): | ||
return GamutB | ||
elif modelId in ('LCT010', 'LCT011', 'LCT012', 'LCT014', 'LCT015', 'LCT016', 'LLC020', 'LST002'): | ||
return GamutC | ||
else: | ||
raise ValueError | ||
return None | ||
|
||
|
||
class ColorHelper: | ||
|
||
def __init__(self, gamut=GamutB): | ||
self.Red = gamut[0] | ||
self.Lime = gamut[1] | ||
self.Blue = gamut[2] | ||
|
||
def hex_to_red(self, hex): | ||
"""Parses a valid hex color string and returns the Red RGB integer value.""" | ||
return int(hex[0:2], 16) | ||
|
||
def hex_to_green(self, hex): | ||
"""Parses a valid hex color string and returns the Green RGB integer value.""" | ||
return int(hex[2:4], 16) | ||
|
||
def hex_to_blue(self, hex): | ||
"""Parses a valid hex color string and returns the Blue RGB integer value.""" | ||
return int(hex[4:6], 16) | ||
|
||
def hex_to_rgb(self, h): | ||
"""Converts a valid hex color string to an RGB array.""" | ||
rgb = (self.hex_to_red(h), self.hex_to_green(h), self.hex_to_blue(h)) | ||
return rgb | ||
|
||
def rgb_to_hex(self, r, g, b): | ||
"""Converts RGB to hex.""" | ||
return '%02x%02x%02x' % (r, g, b) | ||
|
||
def random_rgb_value(self): | ||
"""Return a random Integer in the range of 0 to 255, representing an RGB color value.""" | ||
return random.randrange(0, 256) | ||
|
||
def cross_product(self, p1, p2): | ||
"""Returns the cross product of two XYPoints.""" | ||
return (p1.x * p2.y - p1.y * p2.x) | ||
|
||
def check_point_in_lamps_reach(self, p): | ||
"""Check if the provided XYPoint can be recreated by a Hue lamp.""" | ||
v1 = XYPoint(self.Lime.x - self.Red.x, self.Lime.y - self.Red.y) | ||
v2 = XYPoint(self.Blue.x - self.Red.x, self.Blue.y - self.Red.y) | ||
|
||
q = XYPoint(p.x - self.Red.x, p.y - self.Red.y) | ||
s = self.cross_product(q, v2) / self.cross_product(v1, v2) | ||
t = self.cross_product(v1, q) / self.cross_product(v1, v2) | ||
|
||
return (s >= 0.0) and (t >= 0.0) and (s + t <= 1.0) | ||
|
||
def get_closest_point_to_line(self, A, B, P): | ||
"""Find the closest point on a line. This point will be reproducible by a Hue lamp.""" | ||
AP = XYPoint(P.x - A.x, P.y - A.y) | ||
AB = XYPoint(B.x - A.x, B.y - A.y) | ||
ab2 = AB.x * AB.x + AB.y * AB.y | ||
ap_ab = AP.x * AB.x + AP.y * AB.y | ||
t = ap_ab / ab2 | ||
|
||
if t < 0.0: | ||
t = 0.0 | ||
elif t > 1.0: | ||
t = 1.0 | ||
|
||
return XYPoint(A.x + AB.x * t, A.y + AB.y * t) | ||
|
||
def get_closest_point_to_point(self, xy_point): | ||
# Color is unreproducible, find the closest point on each line in the CIE 1931 'triangle'. | ||
pAB = self.get_closest_point_to_line(self.Red, self.Lime, xy_point) | ||
pAC = self.get_closest_point_to_line(self.Blue, self.Red, xy_point) | ||
pBC = self.get_closest_point_to_line(self.Lime, self.Blue, xy_point) | ||
|
||
# Get the distances per point and see which point is closer to our Point. | ||
dAB = self.get_distance_between_two_points(xy_point, pAB) | ||
dAC = self.get_distance_between_two_points(xy_point, pAC) | ||
dBC = self.get_distance_between_two_points(xy_point, pBC) | ||
|
||
lowest = dAB | ||
closest_point = pAB | ||
|
||
if (dAC < lowest): | ||
lowest = dAC | ||
closest_point = pAC | ||
|
||
if (dBC < lowest): | ||
lowest = dBC | ||
closest_point = pBC | ||
|
||
# Change the xy value to a value which is within the reach of the lamp. | ||
cx = closest_point.x | ||
cy = closest_point.y | ||
|
||
return XYPoint(cx, cy) | ||
|
||
def get_distance_between_two_points(self, one, two): | ||
"""Returns the distance between two XYPoints.""" | ||
dx = one.x - two.x | ||
dy = one.y - two.y | ||
return math.sqrt(dx * dx + dy * dy) | ||
|
||
def get_xy_point_from_rgb(self, red_i, green_i, blue_i): | ||
"""Returns an XYPoint object containing the closest available CIE 1931 x, y coordinates | ||
based on the RGB input values.""" | ||
|
||
red = red_i / 255.0 | ||
green = green_i / 255.0 | ||
blue = blue_i / 255.0 | ||
|
||
r = ((red + 0.055) / (1.0 + 0.055))**2.4 if (red > 0.04045) else (red / 12.92) | ||
g = ((green + 0.055) / (1.0 + 0.055))**2.4 if (green > 0.04045) else (green / 12.92) | ||
b = ((blue + 0.055) / (1.0 + 0.055))**2.4 if (blue > 0.04045) else (blue / 12.92) | ||
|
||
X = r * 0.664511 + g * 0.154324 + b * 0.162028 | ||
Y = r * 0.283881 + g * 0.668433 + b * 0.047685 | ||
Z = r * 0.000088 + g * 0.072310 + b * 0.986039 | ||
|
||
# @TUnbehaun added if statement to avoid division by zero error | ||
if (X + Y + Z == 0): | ||
cx = 0 | ||
cy = 0 | ||
else: | ||
cx = X / (X + Y + Z) | ||
cy = Y / (X + Y + Z) | ||
|
||
# Check if the given XY value is within the colourreach of our lamps. | ||
xy_point = XYPoint(cx, cy) | ||
in_reach = self.check_point_in_lamps_reach(xy_point) | ||
|
||
if not in_reach: | ||
xy_point = self.get_closest_point_to_point(xy_point) | ||
|
||
return xy_point | ||
|
||
def get_rgb_from_xy_and_brightness(self, x, y, bri=1): | ||
"""Inverse of `get_xy_point_from_rgb`. Returns (r, g, b) for given x, y values. | ||
Implementation of the instructions found on the Philips Hue iOS SDK docs: http://goo.gl/kWKXKl | ||
""" | ||
# The xy to color conversion is almost the same, but in reverse order. | ||
# Check if the xy value is within the color gamut of the lamp. | ||
# If not continue with step 2, otherwise step 3. | ||
# We do this to calculate the most accurate color the given light can actually do. | ||
xy_point = XYPoint(x, y) | ||
|
||
if not self.check_point_in_lamps_reach(xy_point): | ||
# Calculate the closest point on the color gamut triangle | ||
# and use that as xy value See step 6 of color to xy. | ||
xy_point = self.get_closest_point_to_point(xy_point) | ||
|
||
# Calculate XYZ values Convert using the following formulas: | ||
Y = bri | ||
X = (Y / xy_point.y) * xy_point.x | ||
Z = (Y / xy_point.y) * (1 - xy_point.x - xy_point.y) | ||
|
||
# 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]) | ||
|
||
r, g, b = map(lambda x: int(x * 255), [r, g, b]) | ||
|
||
# Convert the RGB values to your color object The rgb values from the above formulas are between 0.0 and 1.0. | ||
return (r, g, b) | ||
|
||
|
||
class Converter: | ||
|
||
def __init__(self, gamut=GamutB): | ||
self.color = ColorHelper(gamut) | ||
|
||
def hex_to_xy(self, h): | ||
"""Converts hexadecimal colors represented as a String to approximate CIE | ||
1931 x and y coordinates. | ||
""" | ||
rgb = self.color.hex_to_rgb(h) | ||
return self.rgb_to_xy(rgb[0], rgb[1], rgb[2]) | ||
|
||
def rgb_to_xy(self, red, green, blue): | ||
"""Converts red, green and blue integer values to approximate CIE 1931 | ||
x and y coordinates. | ||
""" | ||
point = self.color.get_xy_point_from_rgb(red, green, blue) | ||
return (point.x, point.y) | ||
|
||
def xy_to_hex(self, x, y, bri=1): | ||
"""Converts CIE 1931 x and y coordinates and brightness value from 0 to 1 | ||
to a CSS hex color.""" | ||
r, g, b = self.color.get_rgb_from_xy_and_brightness(x, y, bri) | ||
return self.color.rgb_to_hex(r, g, b) | ||
|
||
def xy_to_rgb(self, x, y, bri=1): | ||
"""Converts CIE 1931 x and y coordinates and brightness value from 0 to 1 | ||
to a CSS hex color.""" | ||
r, g, b = self.color.get_rgb_from_xy_and_brightness(x, y, bri) | ||
return (r, g, b) | ||
|
||
def get_random_xy_color(self): | ||
"""Returns the approximate CIE 1931 x,y coordinates represented by the | ||
supplied hexColor parameter, or of a random color if the parameter | ||
is not passed.""" | ||
r = self.color.random_rgb_value() | ||
g = self.color.random_rgb_value() | ||
b = self.color.random_rgb_value() | ||
return self.rgb_to_xy(r, g, b) |
Oops, something went wrong.