Created
December 4, 2022 16:57
-
-
Save d8ahazard/cfca425022d8e36d6db0e54f608f9263 to your computer and use it in GitHub Desktop.
Home Assistant PyScript - ColorSwarm For ALl
This file contains hidden or 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
""" | |
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 logging | |
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) | |
run_swarm = True | |
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": 5, | |
"max_hold_secs": 15, | |
"type": "all", | |
"palette": [ | |
{ | |
# Orange | |
"rgb_color": (252, 69, 3), | |
"brightness": 255, | |
}, | |
{ | |
# Puuuurple | |
"rgb_color": (102, 0, 166), | |
"brightness": 255, | |
}, | |
{ | |
# Blue | |
"rgb_color": (23, 0, 171), | |
"brightness": 255, | |
} | |
], | |
}, | |
"Fall": { | |
"transition_secs": 5, | |
"max_hold_secs": 10, | |
"type": "all", | |
"palette": [ | |
{ | |
# Red | |
"rgb_color": (252, 0, 5), | |
"brightness": 128 | |
}, | |
{ | |
# Red-Orange | |
"rgb_color": (252, 50, 5), | |
"brightness": 128 | |
}, | |
{ | |
# Orange | |
"rgb_color": (255, 100, 5), | |
"brightness": 128 | |
}, | |
{ | |
# Yellow-Orange | |
"rgb_color": (250, 150, 5), | |
"brightness": 128 | |
}, | |
{ | |
# Yellow | |
"rgb_color": (250, 200, 5), | |
"brightness": 128 | |
}, | |
{ | |
# Yellow-Orange | |
"rgb_color": (250, 150, 5), | |
"brightness": 128 | |
}, | |
{ | |
# Orange | |
"rgb_color": (250, 100, 5), | |
"brightness": 128 | |
}, | |
{ | |
# Red-Orange | |
"rgb_color": (255, 50, 5), | |
"brightness": 128 | |
} | |
], | |
}, | |
"Xmas": { | |
"transition_secs": 5, | |
"max_hold_secs": 10, | |
"type": "linear", | |
"palette": [ | |
{ | |
# Red | |
"rgb_color": (220, 0, 0), | |
"brightness": 128 | |
}, | |
{ | |
# Green | |
"rgb_color": (0, 255, 0), | |
"brightness": 128 | |
} | |
], | |
}, | |
} | |
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: List 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 = [] | |
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") | |
if "hs" in modes: | |
entity_ids.append(entity.entity_id) | |
log.info(f"Returning id list: {sorted(entity_ids)}") | |
return sorted(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. | |
""" | |
global run_swarm | |
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] | |
anim_type = "random" | |
color_idx = 0 | |
if 'type' in swarm: | |
anim_type = swarm["type"] | |
ent_times = {} | |
run_swarm = True | |
# This will loop forever as long as the task isn't killed. | |
now = time.monotonic() | |
trans_time = swarm["transition_secs"] | |
max_time = swarm["max_hold_secs"] | |
next_time = max_time + trans_time + now | |
for entity_id in entity_ids: | |
if anim_type == "random": | |
next_time = trans_time + random.uniform(now, now + max_time) | |
ent_times[entity_id] = next_time | |
start_idx = 0 | |
while run_swarm: | |
ent_idx = 0 | |
color_idx = start_idx | |
for entity_id in entity_ids: | |
if anim_type == "random": | |
sleep_time = trans_time + random.uniform(0, max_time) | |
next_color = random.choice(swarm["palette"]) | |
else: | |
sleep_time = max_time + trans_time | |
if anim_type == "linear": | |
color_idx += 1 | |
if ent_idx == 0: | |
start_idx += 1 | |
if start_idx >= len(swarm["palette"]): | |
start_idx = 0 | |
else: | |
if ent_idx == 0: | |
color_idx += 1 | |
if color_idx >= len(swarm["palette"]): | |
color_idx = 0 | |
next_color = swarm["palette"][color_idx] | |
ent_times[entity_id] = next_time | |
light_args = { | |
"entity_id": entity_id, | |
"transition": trans_time, | |
**next_color, | |
} | |
light.turn_on(**light_args) | |
if anim_type is "random": | |
task.sleep(sleep_time) | |
else: | |
if ent_idx == len(entity_ids) - 1: | |
task.sleep(sleep_time) | |
ent_idx += 1 | |
log.info("Swarm stopped.") | |
@service | |
def color_swarm_turn_off(area_id="Office"): | |
global run_swarm | |
run_swarm = False | |
"""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.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment