Skip to content

Instantly share code, notes, and snippets.

@d8ahazard
Created December 4, 2022 16:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save d8ahazard/cfca425022d8e36d6db0e54f608f9263 to your computer and use it in GitHub Desktop.
Save d8ahazard/cfca425022d8e36d6db0e54f608f9263 to your computer and use it in GitHub Desktop.
Home Assistant PyScript - ColorSwarm For ALl
"""
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