456 lines
13 KiB
Python
456 lines
13 KiB
Python
"""
|
|
A collection of lighting effects that runs asynchronously on Philips Hue rooms/groups.
|
|
Pyscript must be configured to expose the "hass" global variable and allow all imports
|
|
so that we can access the Hue bridge configs and entity registry.
|
|
"""
|
|
import heapq
|
|
import random
|
|
import time
|
|
|
|
import homeassistant
|
|
from homeassistant.helpers import device_registry as dr
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
devreg = homeassistant.helpers.device_registry.async_get(hass)
|
|
entreg = homeassistant.helpers.entity_registry.async_get(hass)
|
|
|
|
swarm_groups = {}
|
|
# Swarm definitions. Add your own here. To favor a particular color, add multiple instances of it to the palette.
|
|
# Max hold is the maximum number of seconds a bulb will hold its setting before transitioning to a new random color.
|
|
# The other attributes are self-explanatory, I hope.
|
|
swarms = {
|
|
"Christmas": {
|
|
"transition_secs": 10,
|
|
"max_hold_secs": 60,
|
|
"palette": [
|
|
{
|
|
"rgb_color": (255, 0, 0),
|
|
"brightness": 100,
|
|
},
|
|
{
|
|
"rgb_color": (0, 255, 0),
|
|
"brightness": 100,
|
|
},
|
|
],
|
|
},
|
|
"Bright Christmas": {
|
|
"transition_secs": 1,
|
|
"max_hold_secs": 5,
|
|
"palette": [
|
|
{
|
|
"rgb_color": (255, 13, 24),
|
|
"brightness": 240,
|
|
},
|
|
{
|
|
"rgb_color": (255, 0, 0),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
"rgb_color": (0, 255, 0),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
"rgb_color": (21, 255, 13),
|
|
"brightness": 240,
|
|
},
|
|
],
|
|
},
|
|
"Casino": {
|
|
"transition_secs": 10,
|
|
"max_hold_secs": 60,
|
|
"palette": [
|
|
{
|
|
# Magenta
|
|
"rgb_color": (255, 40, 230),
|
|
"brightness": 214,
|
|
},
|
|
{
|
|
# Blue
|
|
"rgb_color": (70, 82, 255),
|
|
"brightness": 145,
|
|
},
|
|
{
|
|
# Gold
|
|
"rgb_color": (255, 163, 49),
|
|
"brightness": 206,
|
|
},
|
|
{
|
|
# Lavender
|
|
"rgb_color": (115, 56, 255),
|
|
"brightness": 255,
|
|
},
|
|
],
|
|
},
|
|
"Dim arcade": {
|
|
"transition_secs": 10,
|
|
"max_hold_secs": 60,
|
|
"palette": [
|
|
{
|
|
# White-ish
|
|
"rgb_color": (245, 215, 255),
|
|
"brightness": 88,
|
|
},
|
|
{
|
|
# Blue
|
|
"rgb_color": (64, 29, 255),
|
|
"brightness": 226,
|
|
},
|
|
{
|
|
# Red
|
|
"rgb_color": (255, 71, 44),
|
|
"brightness": 70,
|
|
},
|
|
{
|
|
# Purple
|
|
"rgb_color": (117, 12, 255),
|
|
"brightness": 130,
|
|
},
|
|
],
|
|
},
|
|
"Neon sea": {
|
|
"transition_secs": 10,
|
|
"max_hold_secs": 60,
|
|
"palette": [
|
|
{
|
|
# Blue 1
|
|
"rgb_color": (65, 8, 255),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
# Blue 2
|
|
"rgb_color": (64, 10, 255),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
# Sea green
|
|
"rgb_color": (119, 255, 200),
|
|
"brightness": 255,
|
|
},
|
|
],
|
|
},
|
|
"Ocean city": {
|
|
"transition_secs": 10,
|
|
"max_hold_secs": 60,
|
|
"palette": [
|
|
{
|
|
# White-ish
|
|
"rgb_color": (255, 246, 250),
|
|
"brightness": 96,
|
|
},
|
|
{
|
|
# Salmon
|
|
"rgb_color": (255, 171, 89),
|
|
"brightness": 130,
|
|
},
|
|
{
|
|
# Light blue
|
|
"rgb_color": (61, 125, 255),
|
|
"brightness": 120,
|
|
},
|
|
{
|
|
# Dark blue
|
|
"rgb_color": (63, 44, 255),
|
|
"brightness": 83,
|
|
},
|
|
],
|
|
},
|
|
"Murder": {
|
|
"transition_secs": 1,
|
|
"max_hold_secs": 8,
|
|
"palette": [
|
|
{
|
|
"rgb_color": (255, 56, 18),
|
|
"brightness": 55,
|
|
},
|
|
{
|
|
"rgb_color": (255, 53, 4),
|
|
"brightness": 18,
|
|
},
|
|
{
|
|
"rgb_color": (255, 58, 21),
|
|
"brightness": 40,
|
|
},
|
|
{
|
|
"rgb_color": (255, 51, 0),
|
|
"brightness": 54,
|
|
},
|
|
],
|
|
},
|
|
"Purple rain": {
|
|
"transition_secs": 1,
|
|
"max_hold_secs": 8,
|
|
"palette": [
|
|
{
|
|
"rgb_color": (153, 116, 255),
|
|
"brightness": 110,
|
|
},
|
|
{
|
|
"rgb_color": (195, 67, 255),
|
|
"brightness": 62,
|
|
},
|
|
{
|
|
"rgb_color": (163, 82, 255),
|
|
"brightness": 106,
|
|
},
|
|
{
|
|
"rgb_color": (152, 20, 255),
|
|
"brightness": 80,
|
|
},
|
|
],
|
|
},
|
|
"Grad party": {
|
|
"transition_secs": 1,
|
|
"max_hold_secs": 30,
|
|
"palette": [
|
|
{
|
|
# Blackhawk (sorta)
|
|
"rgb_color": (64, 0, 255),
|
|
"brightness": 163,
|
|
},
|
|
{
|
|
# Gold
|
|
"rgb_color": (255, 205, 49),
|
|
"brightness": 240,
|
|
},
|
|
]
|
|
+ [
|
|
{
|
|
# White
|
|
"kelvin": 3200,
|
|
"brightness": 255,
|
|
},
|
|
]
|
|
* 10,
|
|
},
|
|
"USA": {
|
|
"transition_secs": 3,
|
|
"max_hold_secs": 60,
|
|
"palette": [
|
|
{
|
|
"rgb_color": (255, 0, 0),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
"rgb_color": (0, 0, 255),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
"rgb_color": (255, 255, 255),
|
|
"brightness": 255,
|
|
},
|
|
],
|
|
},
|
|
"Northern lights": {
|
|
"transition_secs": 1,
|
|
"max_hold_secs": 8,
|
|
"palette": [
|
|
{
|
|
"rgb_color": (23, 35, 71),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
"rgb_color": (2, 83, 133),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
"rgb_color": (14, 243, 197),
|
|
"brightness": 200,
|
|
},
|
|
{
|
|
"rgb_color": (4, 226, 183),
|
|
"brightness": 200,
|
|
},
|
|
{
|
|
"rgb_color": (3, 132, 152),
|
|
"brightness": 220,
|
|
},
|
|
{
|
|
"rgb_color": (1, 82, 104),
|
|
"brightness": 255,
|
|
},
|
|
],
|
|
},
|
|
"Summer night": {
|
|
"transition_secs": 10,
|
|
"max_hold_secs": 60,
|
|
"palette": [
|
|
{
|
|
"rgb_color": (160, 82, 255),
|
|
"brightness": 28,
|
|
},
|
|
{
|
|
"rgb_color": (96, 84, 255),
|
|
"brightness": 1,
|
|
},
|
|
],
|
|
},
|
|
"Candlelight": {
|
|
"transition_secs": 0.25,
|
|
"max_hold_secs": 4,
|
|
"palette": [
|
|
{
|
|
"color_temp": 2300,
|
|
"brightness": 22,
|
|
},
|
|
{
|
|
"color_temp": 2100,
|
|
"brightness": 48,
|
|
},
|
|
{
|
|
"color_temp": 2200,
|
|
"brightness": 67,
|
|
},
|
|
{
|
|
"color_temp": 3200,
|
|
"brightness": 42,
|
|
},
|
|
{
|
|
"color_temp": 1500,
|
|
"brightness": 22,
|
|
},
|
|
{
|
|
"color_temp": 4500,
|
|
"brightness": 70,
|
|
},
|
|
],
|
|
},
|
|
"Velvet rose": {
|
|
"transition_secs": 10,
|
|
"max_hold_secs": 60,
|
|
"palette": [
|
|
{
|
|
"rgb_color": (255, 125, 162),
|
|
"brightness": 64,
|
|
},
|
|
{
|
|
"rgb_color": (255, 111, 169),
|
|
"brightness": 64,
|
|
},
|
|
{
|
|
"rgb_color": (239, 125, 255),
|
|
"brightness": 64,
|
|
},
|
|
{
|
|
"rgb_color": (255, 134, 116),
|
|
"brightness": 64,
|
|
},
|
|
{
|
|
"rgb_color": (255, 147, 185),
|
|
"brightness": 64,
|
|
},
|
|
],
|
|
},
|
|
"Halloween": {
|
|
"transition_secs": 4,
|
|
"max_hold_secs": 10,
|
|
"palette": [
|
|
{
|
|
# Orange
|
|
"rgb_color": (247, 95, 28),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
# Light Orange
|
|
"rgb_color": (255, 154, 0),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
# Puuuurple
|
|
"rgb_color": (136, 30, 228),
|
|
"brightness": 255,
|
|
},
|
|
{
|
|
# Green
|
|
"rgb_color": (133, 226, 31),
|
|
"brightness": 255,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def light_entities_for_area(tgt_area_name):
|
|
"""Find light entity IDs for a specified area. Assumes all lights are color-changing.
|
|
|
|
:param tgt_area_name: The HA Area containing the lights.
|
|
:return: Set of light entity IDs for the group name or empty set if no matching group or entities are found.
|
|
"""
|
|
log.info(f"Searching for entities in {tgt_area_name}")
|
|
entity_ids = set()
|
|
entities = er.async_entries_for_area(entreg, tgt_area_name)
|
|
if entities:
|
|
entities.extend([e for x in dr.async_entries_for_area(devreg, tgt_area_name) for e in
|
|
homeassistant.helpers.entity_registry.async_entries_for_device(entreg, x.id)])
|
|
|
|
for entity in entities:
|
|
if "light" in entity.entity_id:
|
|
modes = entity.capabilities.get("supported_color_modes")
|
|
log.info(f"Area entity: {entity.id}, modes - {modes}")
|
|
if "hs" in modes:
|
|
entity_ids.add(entity.entity_id)
|
|
return entity_ids
|
|
|
|
|
|
@service
|
|
def color_swarm_turn_on(area_id="Office", swarm_name="Christmas"):
|
|
"""Start the color swarm effect on the specified Philips Hue light group.
|
|
|
|
The color swarm continues running on the group until it is turned off or turned on with different parameters.
|
|
|
|
:param area_id: ID Of the HA Area to control. Case-sensitive.
|
|
:param swarm_name: The predefined swarm definition including color palette and transitions.
|
|
"""
|
|
|
|
if swarm_name not in swarms:
|
|
raise ValueError(f"Swarm '{swarm_name}' does not exist.")
|
|
task.unique(f"color-swarm-{area_id}")
|
|
entity_ids = light_entities_for_area(area_id)
|
|
if entity_ids:
|
|
log.info(
|
|
f"Started '{swarm_name}' color swarm for area '{area_id}' consisting of {len(entity_ids)} light(s)."
|
|
)
|
|
else:
|
|
log.error(f"No light entities found for area '{area_id}'.")
|
|
swarm_groups[area_id] = entity_ids
|
|
# Create a priority queue of the next transition per light, sorted by random future transition times.
|
|
swarm = swarms[swarm_name]
|
|
transition_q = []
|
|
start_time = time.monotonic()
|
|
for entity_id in entity_ids:
|
|
change_time = random.uniform(start_time, start_time + swarm["max_hold_secs"])
|
|
change_color = random.choice(swarm["palette"])
|
|
heapq.heappush(transition_q, (change_time, entity_id, change_color))
|
|
|
|
# This will loop forever as long as there are lights and the task isn't killed.
|
|
while transition_q:
|
|
head_time, entity_id, head_color = heapq.heappop(transition_q)
|
|
now = time.monotonic()
|
|
if head_time > now:
|
|
task.sleep(head_time - now)
|
|
light_args = {
|
|
"entity_id": entity_id,
|
|
"transition": swarm["transition_secs"],
|
|
**head_color,
|
|
}
|
|
light.turn_on(**light_args)
|
|
log.debug(f"Applied transition: {light_args}")
|
|
now = time.monotonic()
|
|
next_time = swarm["transition_secs"] + random.uniform(now, now + swarm["max_hold_secs"])
|
|
next_color = random.choice(swarm["palette"])
|
|
heapq.heappush(transition_q, (next_time, entity_id, next_color))
|
|
|
|
|
|
@service
|
|
def color_swarm_turn_off(area_id="Office"):
|
|
"""Stop any running color swarm effect on the specified area."""
|
|
log.info(f"Stopping swarm: {area_id}")
|
|
task.unique(f"color-swarm-{area_id}")
|
|
if area_id in swarm_groups:
|
|
entities = swarm_groups[area_id]
|
|
for entity in entities:
|
|
light_args = {
|
|
"entity_id": entity,
|
|
"transition": 0
|
|
}
|
|
light.turn_off(**light_args)
|
|
log.info(f"Stopped lights for {len(entities)} lights.") |