# 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 }}"
