diff --git a/Common/Light Color Mapper/README.md b/Common/Light Color Mapper/README.md new file mode 100644 index 0000000..6068a0f --- /dev/null +++ b/Common/Light Color Mapper/README.md @@ -0,0 +1,67 @@ +# Light Color Mapper + +Syncs light colors in real-time from color sensor entities. Sensors and lights are paired by list index (sensor 0 → light 0, sensor 1 → light 1, etc.). + +## Features + +- Real-time color sync from sensor entities to lights +- Index-based sensor-to-light mapping (add in matching order) +- FPS throttling for performance control (0–60 fps) +- Brightness override or per-sensor brightness from attributes +- Configurable behavior when sensors are unavailable (turn off, keep last, default color) +- Control entity (switch/input_boolean) to enable/disable +- Optional auto-off when the control entity is disabled +- Custom post-update callback action (runs after each update cycle) + +## How It Works + +The automation triggers on any state change from the configured color sensors. Each sensor is paired with a light by list position: + +| Sensor Index | Light Index | +| --- | --- | +| Sensor 0 | Light 0 | +| Sensor 1 | Light 1 | +| Sensor 2 | Light 2 | +| ... | ... | + +When a sensor updates, **all** sensor-light pairs are re-evaluated to keep lights in sync. + +### FPS Throttling + +The automation uses `mode: restart` with a trailing delay to throttle updates: + +- **FPS = 0**: No throttle — every state change triggers an immediate update. +- **FPS > 0**: After processing all lights, a delay of `1000 / FPS` ms is added. If a new sensor state arrives during the delay, the automation restarts with the latest data. This naturally caps updates to the configured FPS. + +### Sensor Requirements + +Each color sensor should expose: + +| Attribute | Description | +| --- | --- | +| **State** | Hex color string (e.g., `#FF8800`) or `None` when not processing | +| `rgb_color` | Color as `[r, g, b]` list (used to set light color) | +| `brightness` | Brightness value 0–255 (used when no override is set) | + +## Configuration + +| Input | Description | +| --- | --- | +| **Control Entity** | Switch or input_boolean that enables/disables the automation | +| **Turn Off Lights on Disable** | Turn off all mapped lights when the control entity is switched off (default: true) | +| **Color Sensors** | List of color sensor entities (index-mapped to lights) | +| **Lights** | List of light entities (index-mapped to sensors) | +| **Maximum FPS** | Update rate limit, 0 = unlimited (default: 0) | +| **Brightness Override** | Fixed brightness for all lights, 0 = use sensor brightness (default: 0) | +| **When Sensor is Unavailable** | Action when sensor is None/unavailable: Turn Off, Keep Last, or Set Default Color | +| **Default Color** | Color to apply when sensor is unavailable and action is "Set Default Color" | +| **Post-Update Action** | Custom action to run after all lights are updated (e.g., fire an event, call a service) | + +## Notes + +- If the sensor and light lists have different lengths, only the shorter count of pairs is used (extra entities are ignored). +- When the control entity is turned off and "Turn Off Lights on Disable" is enabled, only the paired lights (up to `pair_count`) are turned off. + +## Author + +Alexei Dolgolyov () diff --git a/Common/Light Color Mapper/blueprint.yaml b/Common/Light Color Mapper/blueprint.yaml new file mode 100644 index 0000000..317e823 --- /dev/null +++ b/Common/Light Color Mapper/blueprint.yaml @@ -0,0 +1,327 @@ +# Light Color Mapper +# Syncs light colors in real-time from color sensor entities. +# See README.md for detailed documentation. +# +# Author: Alexei Dolgolyov (dolgolyov.alexei@gmail.com) + +blueprint: + name: "Custom: Light Color Mapper" + description: > + Maps color sensor states to lights in real-time. + Sensors and lights are paired by list index (sensor 0 → light 0, etc.). + Supports FPS throttling, brightness override, and configurable behavior + when sensors are unavailable. + domain: automation + + # =========================================================================== + # INPUT CONFIGURATION + # =========================================================================== + input: + + # ------------------------------------------------------------------------- + # Control + # ------------------------------------------------------------------------- + control_group: + name: "Control" + collapsed: false + input: + control_entity: + name: Control Entity + description: "Switch or input_boolean that enables/disables this automation." + selector: + entity: + domain: + - input_boolean + - switch + + turn_off_on_disable: + name: Turn Off Lights on Disable + description: "Turn off all mapped lights when the control entity is switched off." + default: true + selector: + boolean: + + # ------------------------------------------------------------------------- + # Devices + # ------------------------------------------------------------------------- + devices_group: + name: "Devices" + collapsed: false + input: + color_sensors: + name: Color Sensors + description: > + List of color sensor entities. Each sensor's state is a hex color + string (e.g., #FF8800) with attributes: r, g, b, brightness, + rgb_color. Sensors are mapped to lights by list index. + selector: + entity: + domain: sensor + multiple: true + + lights: + name: Lights + description: > + List of light entities. Each light is paired with the sensor at the + same list index (sensor 0 → light 0, sensor 1 → light 1, etc.). + selector: + entity: + domain: light + multiple: true + + # ------------------------------------------------------------------------- + # Settings + # ------------------------------------------------------------------------- + settings_group: + name: "Settings" + collapsed: false + input: + max_fps: + name: Maximum FPS + description: > + Maximum updates per second. With mode: restart, a trailing delay + throttles updates naturally — new state changes during the delay + restart the automation with fresh data. Set to 0 for unlimited. + default: 0 + selector: + number: + min: 0 + max: 60 + mode: slider + + brightness_override: + name: Brightness Override + description: > + Override brightness for all lights (0–255). + Set to 0 to use each sensor's brightness attribute instead. + default: 0 + selector: + number: + min: 0 + max: 255 + mode: slider + + none_action: + name: When Sensor is Unavailable + description: "Action to take when a sensor state is None, unknown, or unavailable." + default: "turn_off" + selector: + select: + options: + - label: "Turn Off Light" + value: "turn_off" + - label: "Keep Last Color" + value: "keep_last" + - label: "Set Default Color" + value: "default_color" + + default_color: + name: Default Color + description: > + Color to apply when a sensor is unavailable. + Only used when "When Sensor is Unavailable" is set to "Set Default Color". + default: [255, 255, 255] + selector: + color_rgb: + + # ------------------------------------------------------------------------- + # Callback Actions + # ------------------------------------------------------------------------- + actions_group: + name: "Callback Actions" + collapsed: true + input: + post_update_action: + name: Post-Update Action + description: > + Custom action to run after all sensor-light pairs have been processed. + Runs once per update cycle, after all lights are set. + Leave empty to do nothing. + default: [] + selector: + action: + + # ------------------------------------------------------------------------- + # Debug + # ------------------------------------------------------------------------- + debug: + name: "Debug" + collapsed: true + input: + enable_debug_notifications: + name: Enable Debug Notifications + description: > + Send persistent notifications showing sensor states and light + updates for troubleshooting. + default: false + selector: + boolean: + +# Restart mode ensures the latest sensor state always wins. +# Combined with a trailing delay (FPS throttle), incoming state changes +# during the delay cancel the current run and restart with fresh data. +mode: restart + +# ============================================================================= +# TRIGGERS +# ============================================================================= +trigger: + # Any color sensor changes state + - platform: state + entity_id: !input color_sensors + id: "sensor_changed" + + # Control entity toggled on/off + - platform: state + entity_id: !input control_entity + id: "control_changed" + +# ============================================================================= +# VARIABLES +# ============================================================================= +variables: + # --------------------------------------------------------------------------- + # Input References + # --------------------------------------------------------------------------- + control_entity: !input control_entity + turn_off_on_disable: !input turn_off_on_disable + color_sensors: !input color_sensors + lights: !input lights + max_fps: !input max_fps + brightness_override: !input brightness_override + none_action: !input none_action + default_color: !input default_color + enable_debug_notifications: !input enable_debug_notifications + post_update_action: !input post_update_action + + # --------------------------------------------------------------------------- + # Computed Values + # --------------------------------------------------------------------------- + # Use the shorter list length to avoid index-out-of-range errors + pair_count: "{{ [color_sensors | length, lights | length] | min }}" + +# ============================================================================= +# ACTIONS +# ============================================================================= +action: + + # --------------------------------------------------------------------------- + # Debug Logging + # --------------------------------------------------------------------------- + - choose: + - conditions: + - condition: template + value_template: "{{ enable_debug_notifications }}" + sequence: + - service: persistent_notification.create + data: + title: "Light Color Mapper Debug" + message: > + **Trigger:** {{ trigger.id }} + **Control:** {{ states(control_entity) }} + **Pairs:** {{ pair_count }} + **FPS:** {{ max_fps }} + **Brightness Override:** {{ brightness_override }} + + **Sensor States:** + {% for i in range(pair_count | int) %} + {{ i }}: {{ color_sensors[i] }} = {{ states(color_sensors[i]) }} (rgb: {{ state_attr(color_sensors[i], 'rgb_color') }}, brightness: {{ state_attr(color_sensors[i], 'brightness') }}) + {% endfor %} + + # --------------------------------------------------------------------------- + # Check Control Entity + # --------------------------------------------------------------------------- + - choose: + - conditions: + - condition: template + value_template: "{{ not is_state(control_entity, 'on') }}" + sequence: + # Turn off all mapped lights if configured + - choose: + - conditions: + - condition: template + value_template: "{{ turn_off_on_disable }}" + sequence: + - service: light.turn_off + target: + entity_id: "{{ lights[:pair_count | int] }}" + - stop: "Control entity is off" + + # --------------------------------------------------------------------------- + # Process Each Sensor-Light Pair + # --------------------------------------------------------------------------- + - repeat: + count: "{{ pair_count | int }}" + sequence: + - variables: + idx: "{{ repeat.index - 1 }}" + current_sensor: "{{ color_sensors[idx] }}" + current_light: "{{ lights[idx] }}" + sensor_state: "{{ states(current_sensor) }}" + is_unavailable: "{{ sensor_state in ['None', 'none', 'unknown', 'unavailable', 'undefined'] or sensor_state | length == 0 }}" + + - choose: + # Sensor is unavailable — apply configured action + - conditions: + - condition: template + value_template: "{{ is_unavailable }}" + sequence: + - choose: + # Turn off the light + - conditions: + - condition: template + value_template: "{{ none_action == 'turn_off' }}" + sequence: + - service: light.turn_off + target: + entity_id: "{{ current_light }}" + + # Set default color + - conditions: + - condition: template + value_template: "{{ none_action == 'default_color' }}" + sequence: + - service: light.turn_on + target: + entity_id: "{{ current_light }}" + data: + rgb_color: "{{ default_color }}" + brightness: "{{ brightness_override | int if brightness_override | int > 0 else 255 }}" + + # keep_last — do nothing + + # Sensor has valid state — apply color to light + default: + - variables: + sensor_rgb: "{{ state_attr(current_sensor, 'rgb_color') | default([255, 255, 255]) }}" + sensor_brightness: "{{ state_attr(current_sensor, 'brightness') | default(255) }}" + target_brightness: "{{ brightness_override | int if brightness_override | int > 0 else sensor_brightness | int }}" + + - service: light.turn_on + target: + entity_id: "{{ current_light }}" + data: + rgb_color: "{{ sensor_rgb }}" + brightness: "{{ target_brightness }}" + + # --------------------------------------------------------------------------- + # Post-Update Callback + # --------------------------------------------------------------------------- + - choose: + - conditions: + - condition: template + value_template: "{{ post_update_action | length > 0 }}" + sequence: !input post_update_action + + # --------------------------------------------------------------------------- + # FPS Throttle + # --------------------------------------------------------------------------- + # With mode: restart, new triggers during this delay cancel and restart + # the automation, effectively capping updates to max_fps per second. + - choose: + - conditions: + - condition: template + value_template: "{{ max_fps | int > 0 }}" + sequence: + - delay: + milliseconds: "{{ (1000 / (max_fps | int)) | int }}"