290 lines
10 KiB
YAML
290 lines
10 KiB
YAML
# Stream Dock N4 Pro — the Home Assistant half of the four-dial lighting control.
|
|
#
|
|
# Drop this file in HA's config as `packages/stream_dock.yaml` and make sure
|
|
# configuration.yaml has:
|
|
#
|
|
# homeassistant:
|
|
# packages: !include_dir_named packages
|
|
#
|
|
# `stream-dock/install-ha-package.sh` does both of those for you.
|
|
#
|
|
# WHY THIS EXISTS AT ALL, instead of the dock calling light.turn_on directly:
|
|
# a rotary encoder produces a *relative* movement ("three ticks clockwise"), and
|
|
# Home Assistant has no relative-colour service. There is `brightness_step_pct`,
|
|
# but nothing equivalent for a colour channel — turning the red dial up means
|
|
# reading the lamp's current rgb_color, adding to one element, and writing all
|
|
# three back. That read has to happen where the state lives, which is here. The
|
|
# Stream Deck plugin only ever sends "which channel, how many ticks".
|
|
#
|
|
# Every script takes `entity_id` explicitly rather than baking a room in, so one
|
|
# copy of this package serves every room and a second dock needs no new HA config.
|
|
# The per-room part lives in the dock's own button bindings — see
|
|
# `stream-dock/generated/bindings.md`, which is generated from CoreSystemConfig.json.
|
|
#
|
|
# NOTHING HERE HAS RUN AGAINST A REAL HOME ASSISTANT. It is written from the
|
|
# template documentation and reviewed by hand; the first thing to do once HA is up
|
|
# is run each script from Developer Tools -> Actions with a real light before
|
|
# binding a single dial. See stream-dock/README.md §"What is unverified".
|
|
|
|
script:
|
|
|
|
stream_dock_channel_adjust:
|
|
alias: "Stream Dock: nudge one RGB channel"
|
|
description: >-
|
|
Relative colour change from an encoder. Reads the target's current rgb_color,
|
|
moves one channel by ticks*step, clamps to 0-255 and writes all three back.
|
|
mode: queued
|
|
max: 30
|
|
fields:
|
|
entity_id:
|
|
name: Lights
|
|
description: The light(s) this dock controls. Required — never defaulted.
|
|
required: true
|
|
selector:
|
|
entity:
|
|
multiple: true
|
|
filter:
|
|
domain: light
|
|
channel:
|
|
name: Channel
|
|
required: true
|
|
selector:
|
|
select:
|
|
options: [r, g, b]
|
|
ticks:
|
|
name: Ticks
|
|
description: Encoder movement. Negative is counter-clockwise.
|
|
required: true
|
|
selector:
|
|
number: { min: -64, max: 64, mode: box }
|
|
step:
|
|
name: Step
|
|
description: 0-255 units per tick.
|
|
default: 8
|
|
selector:
|
|
number: { min: 1, max: 64, mode: box }
|
|
variables:
|
|
# entity_id arrives as a list from the dock, but a hand-run from Developer
|
|
# Tools is just as likely to pass a bare string. Normalise both.
|
|
targets: >-
|
|
{{ entity_id if entity_id is not string else [entity_id] }}
|
|
# Which lamp's colour counts as "the current colour" when the dial drives
|
|
# several. A lit one, if there is one — an off lamp reports rgb_color: none
|
|
# in most integrations, and starting from white every time you touch the dial
|
|
# while one lamp of three is off is the bug this line exists to avoid.
|
|
reference: >-
|
|
{{ (expand(targets) | selectattr('state', 'eq', 'on') | map(attribute='entity_id') | list
|
|
+ (targets | list)) | first }}
|
|
base: >-
|
|
{{ (state_attr(reference, 'rgb_color') or [255, 255, 255]) | list }}
|
|
index: "{{ {'r': 0, 'g': 1, 'b': 2}.get(channel, '0') | int }}"
|
|
delta: "{{ ((ticks | float(0)) * (step | float(8))) | round | int }}"
|
|
value: "{{ [[(base[index] | int) + delta, 255] | min, 0] | max }}"
|
|
target_rgb: "{{ base[:index] + [value] + base[index + 1:] }}"
|
|
sequence:
|
|
- action: light.turn_on
|
|
target:
|
|
entity_id: "{{ targets }}"
|
|
data:
|
|
rgb_color: "{{ target_rgb }}"
|
|
|
|
stream_dock_channel_set:
|
|
alias: "Stream Dock: set one RGB channel absolutely"
|
|
description: >-
|
|
The absolute-position variant, for a dial bound to {{ rotationPercent }}
|
|
instead of {{ ticks }}. Same colour maths, but the dial's accumulated
|
|
position IS the channel value rather than a nudge to it.
|
|
mode: queued
|
|
max: 30
|
|
fields:
|
|
entity_id:
|
|
name: Lights
|
|
required: true
|
|
selector:
|
|
entity:
|
|
multiple: true
|
|
filter:
|
|
domain: light
|
|
channel:
|
|
name: Channel
|
|
required: true
|
|
selector:
|
|
select:
|
|
options: [r, g, b]
|
|
percent:
|
|
name: Percent
|
|
description: 0-100, mapped onto 0-255.
|
|
required: true
|
|
selector:
|
|
number: { min: 0, max: 100, mode: box }
|
|
variables:
|
|
targets: >-
|
|
{{ entity_id if entity_id is not string else [entity_id] }}
|
|
reference: >-
|
|
{{ (expand(targets) | selectattr('state', 'eq', 'on') | map(attribute='entity_id') | list
|
|
+ (targets | list)) | first }}
|
|
base: >-
|
|
{{ (state_attr(reference, 'rgb_color') or [255, 255, 255]) | list }}
|
|
index: "{{ {'r': 0, 'g': 1, 'b': 2}.get(channel, '0') | int }}"
|
|
value: "{{ [[((percent | float(0)) * 2.55) | round | int, 255] | min, 0] | max }}"
|
|
target_rgb: "{{ base[:index] + [value] + base[index + 1:] }}"
|
|
sequence:
|
|
- action: light.turn_on
|
|
target:
|
|
entity_id: "{{ targets }}"
|
|
data:
|
|
rgb_color: "{{ target_rgb }}"
|
|
|
|
stream_dock_channel_extreme:
|
|
alias: "Stream Dock: slam one RGB channel to an end stop"
|
|
description: >-
|
|
The dial's push action. A channel that is anywhere above zero goes to zero;
|
|
a channel already at zero goes to full. Two presses always get you back to
|
|
where you were, which is what makes it safe to press by accident.
|
|
mode: queued
|
|
max: 10
|
|
fields:
|
|
entity_id:
|
|
name: Lights
|
|
required: true
|
|
selector:
|
|
entity:
|
|
multiple: true
|
|
filter:
|
|
domain: light
|
|
channel:
|
|
name: Channel
|
|
required: true
|
|
selector:
|
|
select:
|
|
options: [r, g, b]
|
|
variables:
|
|
targets: >-
|
|
{{ entity_id if entity_id is not string else [entity_id] }}
|
|
reference: >-
|
|
{{ (expand(targets) | selectattr('state', 'eq', 'on') | map(attribute='entity_id') | list
|
|
+ (targets | list)) | first }}
|
|
base: >-
|
|
{{ (state_attr(reference, 'rgb_color') or [255, 255, 255]) | list }}
|
|
index: "{{ {'r': 0, 'g': 1, 'b': 2}.get(channel, '0') | int }}"
|
|
value: "{{ 0 if (base[index] | int) > 0 else 255 }}"
|
|
target_rgb: "{{ base[:index] + [value] + base[index + 1:] }}"
|
|
sequence:
|
|
- action: light.turn_on
|
|
target:
|
|
entity_id: "{{ targets }}"
|
|
data:
|
|
rgb_color: "{{ target_rgb }}"
|
|
|
|
stream_dock_brightness_adjust:
|
|
alias: "Stream Dock: nudge brightness"
|
|
description: >-
|
|
The fourth dial. Unlike colour, HA has a native relative service parameter
|
|
for this (brightness_step_pct), so there is no read-modify-write here — and
|
|
it is the integration, not this script, that decides what 0% means (most
|
|
turn the lamp off).
|
|
mode: queued
|
|
max: 30
|
|
fields:
|
|
entity_id:
|
|
name: Lights
|
|
required: true
|
|
selector:
|
|
entity:
|
|
multiple: true
|
|
filter:
|
|
domain: light
|
|
ticks:
|
|
name: Ticks
|
|
required: true
|
|
selector:
|
|
number: { min: -64, max: 64, mode: box }
|
|
step_pct:
|
|
name: Step
|
|
description: Percentage points per tick.
|
|
default: 5
|
|
selector:
|
|
number: { min: 1, max: 50, mode: box }
|
|
variables:
|
|
targets: >-
|
|
{{ entity_id if entity_id is not string else [entity_id] }}
|
|
delta: "{{ ((ticks | float(0)) * (step_pct | float(5))) | round | int }}"
|
|
sequence:
|
|
- action: light.turn_on
|
|
target:
|
|
entity_id: "{{ targets }}"
|
|
data:
|
|
brightness_step_pct: "{{ delta }}"
|
|
|
|
stream_dock_toggle:
|
|
alias: "Stream Dock: toggle the room"
|
|
mode: single
|
|
fields:
|
|
entity_id:
|
|
name: Lights
|
|
required: true
|
|
selector:
|
|
entity:
|
|
multiple: true
|
|
filter:
|
|
domain: light
|
|
sequence:
|
|
- action: light.toggle
|
|
target:
|
|
entity_id: >-
|
|
{{ entity_id if entity_id is not string else [entity_id] }}
|
|
|
|
stream_dock_set_color:
|
|
alias: "Stream Dock: set a preset colour"
|
|
description: >-
|
|
The key presets. brightness_pct is optional: left out, the lamp keeps
|
|
whatever brightness it already had, which is what you want from a key that
|
|
only means "make it green".
|
|
mode: queued
|
|
max: 10
|
|
fields:
|
|
entity_id:
|
|
name: Lights
|
|
required: true
|
|
selector:
|
|
entity:
|
|
multiple: true
|
|
filter:
|
|
domain: light
|
|
rgb:
|
|
name: RGB
|
|
description: "[r, g, b], each 0-255."
|
|
required: true
|
|
selector:
|
|
object:
|
|
brightness_pct:
|
|
name: Brightness
|
|
description: Omit to leave brightness alone.
|
|
selector:
|
|
number: { min: 1, max: 100, mode: box }
|
|
variables:
|
|
targets: >-
|
|
{{ entity_id if entity_id is not string else [entity_id] }}
|
|
sequence:
|
|
# Two branches rather than one templated `data:` block. A template that returns
|
|
# a whole service-data mapping is a trick that may or may not be supported
|
|
# depending on the HA version, and this script is the one thing on the dock that
|
|
# eight keys depend on — so it uses only the boring construct.
|
|
- choose:
|
|
- conditions:
|
|
- condition: template
|
|
value_template: "{{ brightness_pct is defined and brightness_pct not in [none, ''] }}"
|
|
sequence:
|
|
- action: light.turn_on
|
|
target:
|
|
entity_id: "{{ targets }}"
|
|
data:
|
|
rgb_color: "{{ rgb }}"
|
|
brightness_pct: "{{ brightness_pct | int }}"
|
|
default:
|
|
- action: light.turn_on
|
|
target:
|
|
entity_id: "{{ targets }}"
|
|
data:
|
|
rgb_color: "{{ rgb }}"
|