# Curious? https://hackaday.io/project/176690-the-water-watcher
substitutions:
  node_name: waterwatcher55
  wifi_ssid: !secret wifi_ssid
  wifi_password: !secret wifi_password
  # I use the node_name as the MQTT client ID and username
  mqtt_broker: !secret mqtt_broker
  mqtt_password: !secret waterwatcher55_mqtt_password
  big_button: GPIO39  
  log_level: DEBUG

  # For the Atom Matrix, low brightness levels tend to be off-ish instead.
  # Also, color blends, which are not great to begin with, are pretty
  # horrible at low brightness levels. M5, the maker of the Atom Matrix
  # recommends brightness levels no higher than 20% with the FastLED library.
  # I went a little higher for most of these.
  BRIGHTNESS_STARTUP:    25%
  BRIGHTNESS_FULL_FLAME: 25%
  BRIGHTNESS_DAZZLED:    25%
  BRIGHTNESS_PILOT_ON:   25%
  BRIGHTNESS_PILOT_OFF:  20%

  # Need both the quoted and unquoted forms. Thanks, YAML.
  MESSAGE_FULL_FLAME: '"full_flame"'
  NAME_FULL_FLAME:     'full_flame'
  MESSAGE_DAZZLED:    '"dazzled"'
  NAME_DAZZLED:        'dazzled'
  MESSAGE_PILOT_ON:   '"pilot_on"'
  NAME_PILOT_ON:       'pilot_on'
  MESSAGE_PILOT_OFF:  '"pilot_off"'
  NAME_PILOT_OFF:      'pilot_off'
  MESSAGE_STARTUP:    '"startup"'
  NAME_STARTUP:        'startup'

  MQTT_TOPIC:          "waterwatcher/flame"

globals:
  # We need to use a global because this gets used
  # in multiple esphome scopes.
  - id: flame_state_queue
    type: std::queue<std::string>

esphome:
  name: $node_name
  on_boot:
    then:
      # delay a bit for proper initialization before doing any FastLED calls
      - delay: 5s
      # This effect is used at start-up so we know
      # we haven't gotten any flame status yet.
      - lambda: "id(flame_state_queue).push($MESSAGE_STARTUP);"
      - delay: 1s
      - script.execute: react_to_message
esp32:
  board: pico32

wifi:
  networks:
    - ssid: ${wifi_ssid}
      password: ${wifi_password}

sensor:
  - platform: wifi_signal
    name: ${node_name} WiFi Signal

logger:
  level: ${log_level}
ota:

mqtt:
  id: i_mqtt
  broker: ${mqtt_broker}
  client_id: ${node_name}
  username: ${node_name}
  password: ${mqtt_password}
  reboot_timeout: 3000s
  discovery: true
  on_message:
    topic: $MQTT_TOPIC
    then:
      # Messages might come in faster than we can process them, in 
      # which case the flame state might change at some arbitrary
      # point. To avoid the instability, we immediately place the
      # state value in a queue and later process items from that
      # queue. (I don't know if "x" in the lambda is guaranteed
      # to be stable already. I had some trouble when I was doing
      # this with a text sensor subscribed to the topic.)
      - lambda: "id(flame_state_queue).push(x);"
      - script.execute: react_to_message

# This lets you cycle through the display modes
# by pushing the big button. On the M5 Atom Matrix,
# the entire front cover is a button that can be
# pressed. We send it through the queue so that it
# plays well with received MQTT messages.

# The display might switch to something else if an
# MQTT message is received while you are doing this.
binary_sensor:
  - platform: gpio
    pin:
      number: ${big_button}
      inverted: true
    name: ${node_name} Button
    id: big_button
    internal: false
    on_click:
      then:
        - lambda: |-
            static int tick = 0;
            std::string desired_effect;
            switch (tick++ % 5)
            {
              case 0:  desired_effect = $MESSAGE_PILOT_ON;   break;;
              case 1:  desired_effect = $MESSAGE_FULL_FLAME; break;;
              case 2:  desired_effect = $MESSAGE_DAZZLED;    break;;
              case 3:  desired_effect = $MESSAGE_PILOT_OFF;  break;;
              default: desired_effect = $MESSAGE_STARTUP;    break;;
            }
            id(flame_state_queue).push(desired_effect);
        - script.execute: react_to_message

# The M5 Atom Matrix has a 25 led matrix arranged
# in 5x5. If you hold the device with the USB-C
# socket on top, and you are facing the LED panel,
# LED 0 is on the lower right corner. There is a 
# handy chart a bit further below.

# Here is the principle upon which this section is
# organized. Except for the boot-up "startup" effect,
# all of the notifications are done via custom effects.
#
# Each notification type "foo" has a custom effect
# also named "foo". In that custom effect are any
# one-time (per notification) actions, and then a
# companion effect "cycle_foo" is invoked via a call
# to "light.turn_on", which is sort of chaining. 
# Some of those chains eventually chain back to 
# one of the standard effects after some LEDs have
# been lit up.
#
# The custom effect "cycle_foo" is responsible for
# sequencing and timing of any LED pixel art. The
# actual drawing of individual frames is done by
# executing a separately-defined script. The idea of
# that was to make the scripts re-usable. A script
# only draws a single frame of any animation.
#
# The script "react_to_message", which turns on the
# LED effects, first blanks out the LED matrix. These
# custom effects should only worry about blanking
# the LEDs as part of an animation. But for some reason,
# that doesn't work, and they still have to explicitly
# blank any LEDs they want to be off.
#
# (I originally expected more one-time stuff, but 
# it turned out not to be so. I left the structure 
# in place just in case it's needed for some future
# changes.)
light:
  - platform: fastled_clockless
    chipset: WS2812B
    pin: GPIO27
    num_leds: 25
    rgb_order: GRB # required
    name: ${node_name} Matrix
    id: matrix
    effects:
      - addressable_rainbow:
          name: $NAME_STARTUP
          width: 25  # would be better if this defaulted to "num_leds" instead of a constant 50
      - addressable_flicker:
          name: addressable_flicker
          update_interval: 250ms
          intensity: 7%

###################
      - automation:
          name: ${NAME_PILOT_ON}
          sequence:
            - light.turn_on: {id: matrix, brightness: $BRIGHTNESS_PILOT_ON, effect: cycle_$NAME_PILOT_ON}

      - automation:
          # This is on almost all the time, so the animation is intentionally mild.
          name: cycle_$NAME_PILOT_ON
          sequence:
            - script.execute: matrix_blank
            - script.execute: corners_orange
            - light.turn_on: {id: matrix, brightness: $BRIGHTNESS_PILOT_ON, effect: addressable_flicker}

###################
      - automation:
          name: ${NAME_FULL_FLAME}
          sequence:      
            - light.turn_on: {id: matrix, brightness: $BRIGHTNESS_FULL_FLAME, effect: cycle_$NAME_FULL_FLAME}

      - automation:
          # Everything turned on orange-ish. 
          name: cycle_$NAME_FULL_FLAME
          sequence:      
            - script.execute: matrix_blank
            - script.execute: orange_full
            # the standard flicker effect on an all-orange matrix looks like a good flame,
            - light.turn_on: {id: matrix, brightness: $BRIGHTNESS_FULL_FLAME, effect: addressable_flicker}

###################
      - automation:
          name: ${NAME_DAZZLED}
          sequence:
            - light.turn_on: {id: matrix, brightness: $BRIGHTNESS_DAZZLED, effect: cycle_$NAME_DAZZLED}

      - automation:
          name: cycle_$NAME_DAZZLED
          sequence:
            - script.execute: matrix_blank
            - script.execute: orange_center
            - light.turn_on: {id: matrix, brightness: $BRIGHTNESS_DAZZLED, effect: addressable_flicker}

###################
      - automation:
          name: ${NAME_PILOT_OFF}
          sequence:
            - light.turn_on: {id: matrix, brightness: $BRIGHTNESS_PILOT_OFF, effect: cycle_$NAME_PILOT_OFF}

      - automation:
          # When the pilot light goes out, we want a distracting display to catch
          # someone's attention. The color is blue (the universal mnemonic color
          # for cold) and we do some animated flashing.
          name: cycle_$NAME_PILOT_OFF
          sequence:
            - script.execute: blue_full
            - delay: 0.1s
            - script.execute: matrix_blank
            - delay: 0.1s
            - script.execute: blue_even
            - delay: 0.3s
            - script.execute: blue_full
            - delay: 0.1s
            - script.execute: matrix_blank
            - delay: 0.1s
            - script.execute: blue_odd
            - delay: 0.3s

## LED numbering on the M5 Atom Matrix (USB socket on the 20-24 edge)

## 24 23 22 21 20
## 19 18 17 16 15
## 14 13 12 11 10
##  9  8  7  6  5
##  4  3  2  1  0

script:
  - id: blue_even
    then:
      - light.addressable_set: { id: matrix, range_from:  0, range_to:  0, blue: 100%}
      - light.addressable_set: { id: matrix, range_from:  2, range_to:  2, blue: 100%}
      - light.addressable_set: { id: matrix, range_from:  4, range_to:  4, blue: 100%}
      - light.addressable_set: { id: matrix, range_from:  6, range_to:  6, blue: 100%}
      - light.addressable_set: { id: matrix, range_from:  8, range_to:  8, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 10, range_to: 10, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 12, range_to: 12, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 14, range_to: 14, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 16, range_to: 16, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 18, range_to: 18, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 20, range_to: 20, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 22, range_to: 22, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 24, range_to: 24, blue: 100%}

  - id: blue_odd
    then:
      - light.addressable_set: { id: matrix, range_from:  1, range_to:  1, blue: 100%}
      - light.addressable_set: { id: matrix, range_from:  3, range_to:  3, blue: 100%}
      - light.addressable_set: { id: matrix, range_from:  5, range_to:  5, blue: 100%}
      - light.addressable_set: { id: matrix, range_from:  7, range_to:  7, blue: 100%}
      - light.addressable_set: { id: matrix, range_from:  9, range_to:  9, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 11, range_to: 11, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 13, range_to: 13, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 15, range_to: 15, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 17, range_to: 17, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 19, range_to: 19, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 21, range_to: 21, blue: 100%}
      - light.addressable_set: { id: matrix, range_from: 23, range_to: 23, blue: 100%}
    
  - id: blue_full
    then:
      - light.addressable_set: { id: matrix, red: 0%, green: 0%, blue: 100%}

  - id: orange_full
    then:
      - light.addressable_set: { id: matrix, red: 84%, green: 55%, blue: 0%} # all orange

  - id: orange_center
    then:
      - light.addressable_set: { id: matrix, red: 84%, green: 55%, blue: 0%, range_from:  7, range_to:  7}
      - light.addressable_set: { id: matrix, red: 84%, green: 55%, blue: 0%, range_from: 11, range_to: 13}
      - light.addressable_set: { id: matrix, red: 84%, green: 55%, blue: 0%, range_from: 17, range_to: 17}

  - id: matrix_blank
    then:
      - light.addressable_set: { id: matrix, red: 0%, green: 0%, blue: 0%}

  - id: corners_orange
    then:
      - light.addressable_set: { id: matrix, red:  84%, green: 55%, blue: 0%, range_from:  0, range_to:  0}
      - light.addressable_set: { id: matrix, red:  84%, green: 55%, blue: 0%, range_from:  4, range_to:  4}
      - light.addressable_set: { id: matrix, red:  84%, green: 55%, blue: 0%, range_from: 20, range_to: 20}
      - light.addressable_set: { id: matrix, red:  84%, green: 55%, blue: 0%, range_from: 24, range_to: 24}

  - id: react_to_message
    mode: queued  # single-threaded
    then:
      - logger.log: {level: INFO, format: ">>==> FLAME message: %s", args: [id(flame_state_queue).front().c_str()]}
      - if:
          # If it's a "lux:" message, or same as the active effect, or unrecognized, skip it.
          # The reason "lux:" messages are checked specifically instead of being
          # unrecognized is just in case I want to do something in particular with them
          # some day.
          condition:
            lambda: |-
              static std::string active_effect;
              std::string flame_state = id(flame_state_queue).front();
              bool is_lux = flame_state.substr(0,4) == "lux:";
              bool is_same = (flame_state == active_effect);
              bool is_legit = 
                flame_state == $MESSAGE_PILOT_ON    ||
                flame_state == $MESSAGE_PILOT_OFF   ||
                flame_state == $MESSAGE_FULL_FLAME  ||
                flame_state == $MESSAGE_DAZZLED     ||
                flame_state == $MESSAGE_STARTUP;
              if (is_lux  ||  is_same  ||  !is_legit) {
                id(flame_state_queue).pop();
                return false;
              }
              active_effect = flame_state;
              return true;
          then:
            - script.execute: matrix_blank
            - light.turn_on:
                id: matrix
                # Will be one of the recognized flame states
                effect: !lambda |-
                  std::string flame_state = id(flame_state_queue).front();
                  id(flame_state_queue).pop();
                  return flame_state;
