SmartestHome/stream-dock/ha-package/stream_dock.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 }}"