Thanks @studioIngrid, I’ve not tried your code, but it appears to be exactly what I’m after.
CAVEAT: I’m not a programmer
I may be able to chip in on the colour convesion issue that I want to resolve for another use case. The code below is more of an FYI as I can use your code as is, and noting my caveat, It would take me months to integrate it into yours.
To give context to the code, here’s the sequence of how it came about:
- I have RGBW lights that can supports many of the variations of color definitions and color temperature
- I found the Adaptive Lighting integration
- I stopped using the integration because as it turns out, my fully capable RGBW lights would only go to a max mireds of 370 and the light colours when using RGB mode in adaptive lighting didn’t look correct
- I then realised I could do my own colour conversion and use it in automations for my lights that uses the Adaptive Lighting colour temp and brightness profiles, but with my version of the colour conversion
- I found this wikipedia page for an approximation of colour temp in the xy colour space, which I used in a template successfully
- For various reasons, I wanted it to make it overly complicated and frustrating, so I turned to python and colour science
- I hit a various home assistant lockdowns and walls, and ended up having to take the conversion code from the colour science library (many types of conversions).
- I couldn’t get the key component (conversion of colour temp to xy) to work so I mash the colour science code together with the approximation I have above
With that said, this is the gist of how the various code blocks below work:
- The Adaptive Lighting integration must be installed and when configuring in the UI, this checkbox must be checked: include_config_in_attributes
- Template sensors are used to store the colour temp from adaptive lighting as the value and the colour conversions as attributes, but start with placeholders for the values
- When adaptive lighting causes a change to the template sensor value, a generic automation calls the AppDaemon python code for the conversion
- The App then updates the template sensors’ attributes
Note:
- This code will throw errors on startup if you try to turn on lights prior to there being a change in the adpative lighting attributes
- Adaptive lighting isn’t actually required if all you want is the conversion of the colour temp, you’ll just have to change the automation to suit, or take the maths out of the python code and put it in a template sensor
- If you’re starting with Kelvin, to convert to mireds:
mireds = 1,000,000 / Kelvin
I’m happy to answer any questions, but I don’t spend much time on this forum, so my answers will likely be delayed.
Template sensor example (the old school template sensor format is below):
- platform: template
sensors:
adaptive_lighting_mireds_bedroom:
value_template: "{{ state_attr('switch.adaptive_lighting_bedroom', 'color_temp_mired') }}"
attribute_templates:
rgb_colour: 0
hs_colour: 0
xy_colour: 0
kelvin: 0
brightness_pct: "{{ state_attr('switch.adaptive_lighting_bedroom', 'brightness_pct') }}"
Create as many template sensors as you want. The naming is important for the automation, so make sure each sensor name starts with adaptive_lighting_mireds_
and change bedroom
to the various unique identifiers of your choice.
Automation:
- alias: Update adaptive lighting mireds conversion sensors
trigger:
- platform: event
event_type: state_changed
condition:
- condition: template
value_template: '{{ trigger.event.data.entity_id.startswith("sensor.adaptive_lighting_mireds_")
}}'
action:
- choose:
- conditions:
- condition: template
value_template: '{{ is_state(trigger.event.data.entity_id, ''unknown'') }}'
sequence:
- service: persistent_notification.create
data:
title: Adaptive lighting mireds conversion sensor not found
message: The adaptive lighting mireds conversion sensor for {{ trigger.event.data.entity_id
}} could not be found. Please create it first.
default:
- event: adaptive_lighting_mireds_conversion
event_data:
entity_id: '{{ trigger.event.data.entity_id }}'
value: '{{ trigger.event.data.new_state.state }}'
Apps.yaml
AdaptiveLightingMiredsConversion:
module: adaptive_lighting_mireds_conversion
class: AdaptiveLightingMiredsConversion
AppDaemon filename: adaptive_lighting_mireds_conversion.py
in a folder of the same name
adaptive_lighting_mireds_conversion.py
import appdaemon.plugins.hass.hassapi as hass
import numpy as np
class AdaptiveLightingMiredsConversion(hass.Hass):
def initialize(self):
self.listen_event(self.trigger_conversion,
"adaptive_lighting_mireds_conversion")
def trigger_conversion(self, event_name, data, kwargs):
if 'entity_id' in data:
self.sensor_entity_id = data["entity_id"]
mireds = float(data["value"])
cct = 1e6 / mireds
rgb = self.colour_temperature_to_rgb(cct)
hs = self.rgb_to_hs(rgb)
xy = self.cct_to_xy(cct)
kelvin = self.colour_mireds_to_kelvin(cct)
self.log(f"RGB: {rgb}, HS: {hs}, xy: {xy}, kelvin: {kelvin}")
# Update the sensor value
self.set_state(self.sensor_entity_id, state=mireds,
attributes={"rgb_colour": rgb, "hs_colour": hs, "xy_colour": xy, "kelvin": kelvin})
def cct_to_xy(self, cct):
M = 1e6 / cct
if cct < 4000:
x = - 0.2661239 * 1e9 / (cct ** 3) - 0.2343580 * 1e6 / (cct ** 2) + \
0.8776956 * 1e3 / cct + 0.179910
elif cct <= 25000:
x = - 3.0258469 * 1e9 / (cct ** 3) + 2.1070379 * 1e6 / (cct ** 2) + \
0.2226347 * 1e3 / cct + 0.240390
else:
x = - 0.11026621 * (M ** 3) + 0.0516377 * (M ** 2) + \
0.075919 * M + 0.206556
if cct < 2222:
y = - 1.1063814 * (x ** 3) - 1.34811020 * \
(x ** 2) + 2.18555832 * x - 0.20219683
elif 2222 <= cct and cct < 4000:
y = - 0.9549476 * (x ** 3) - 1.37418593 * (x ** 2) + 2.09137015 * x - 0.16748867
elif 4000 <= cct and cct < 25000:
y = 3.0817580 * (x ** 3) - 5.87338670 * \
(x ** 2) + 3.75112997 * x - 0.37001483
else:
y = 1
return x, y
def xyY_to_XYZ(self, xyY):
x, y, Y = xyY
X = (Y / y) * x
Z = (Y / y) * (1 - x - y)
return X, Y, Z
def XYZ_to_sRGB(self, XYZ):
M = np.array([[3.2406, - 1.5372, - 0.4986],
[- 0.9689, 1.8758, 0.0415],
[0.0557, - 0.2040, 1.0570]])
linear_rgb = np.dot(M, XYZ)
return linear_rgb
def gamma_correct_sRGB(self, linear_rgb):
gamma_corrected_rgb = np.where(
linear_rgb <= 0.0031308,
linear_rgb * 12.92,
1.055 * (linear_rgb ** (1 / 2.4)) - 0.055)
return gamma_corrected_rgb
def colour_temperature_to_rgb(self, cct):
# Step 1: Convert colour temperature to xy chromaticity coordinates
xy = self.cct_to_xy(cct)
# Step 2: Convert xyY to XYZ (assuming Y = 1)
Y = 1.0
XYZ = self.xyY_to_XYZ((xy[0], xy[1], Y))
# Step 3: Convert XYZ to linear sRGB
linear_rgb = self.XYZ_to_sRGB(XYZ)
# Step 4: Apply gamma correction
gamma_corrected_rgb = self.gamma_correct_sRGB(linear_rgb)
# Clamp the RGB values to the [0, 1]
# range and convert them to 8 - bit integers
clamped_rgb = np.clip(gamma_corrected_rgb, 0, 1) * 255
# Convert the NumPy array to a list
return clamped_rgb.astype(int).tolist()
def rgb_to_hs(self, rgb):
r, g, b = [val / 255.0 for val in rgb]
max_value = max(r, g, b)
min_value = min(r, g, b)
difference = max_value - min_value
if max_value == min_value:
h = 0
elif max_value == r:
h = (60 * ((g - b) / difference) + 360) % 360
elif max_value == g:
h = (60 * ((b - r) / difference) + 120) % 360
elif max_value == b:
h = (60 * ((r - g) / difference) + 240) % 360
if max_value == 0:
s = 0
else:
s = difference / max_value
return h, s * 100
def colour_mireds_to_kelvin(self, cct):
return cct