# Timers:
#   inactivity: Lights turn off after this time with no human activity.
#   reactivity: If lights are explicitly turned off (not a timeout),
#     they won't automatically turn back on during this period. They
#     can be manually turned on.
#   displaydim: Display is bright after interaction with rotaries. Goes
#     dim after this period. Only applies to normal mode, not metamode.
#   metamode: Display reverts from metamode to normal mode after this
#     period.

# Human activity is using any control or having motion detected:
#    a. If light is off, turn light on
#    b. Restart inactivity timer
#  Active human activity is someone using a control
#  Passive human activity is PIR detection
#    If active human activity,
#         c. display backlight on full
#         d. start display dimout timer

# Display backlight behavior:
#   - full on at power-up
#   - full on for active human activity
#   - full on during final countdown
#   - dim when lights timeout to off
#   - dim after a period of no active human activity

# Rotaries A and B trigger equivalent behaviors:
#    a. Rotation turns dimming up or down.
#    b. Press and rotate turns inactivity timeout up or down
#    c. Single-click switch turns light on or off. If turning off,
#       starts re-activity timer.
#    c. Double-click enters meta-mode for nerdy details. Starts
#       meta-mode timer.

# Naming conventions:
#  underscorioishly all the time
#  scripts start with "S", a number, underscore, and the rest of the name
#    example: S17_blinkity_blink_blink
#  global variables start with "G_" and the rest of the name
#    example: G_how_high_can_you_go
#  global variables that are actually private to a script start with "GS"
#  and the script number, underscore, and the rest of the name
#    example: GS17_how_many_roads
#  most substitutions that really parameter constants are ALL_UPPER_CASE
#  most entity IDs start with "id_"
#  font IDs start with "font_"
#  a period of time is a "duration"; an instant in time is a "time"

substitutions:
  node_name: garag
  wifi_static_ip:  !secret garag_ip
  mqtt_secretword: !secret garag_mqtt_secretword
  mqtt_password:   !secret garag_mqtt_password
  log_level: DEBUG

  ROTARY_STEP_SCALER:                '3'   # one tick is worth this many percent
  DURATION_LIGHTS_STAY_ON:          '90'   # when countdown time is reset, use this value, seconds
  DURATION_COUNTDOWN_TO_LIGHTS_OFF: '30'   # when to start shouting that we're going to turn the lights off
  ROTARY_CLICK_MAX_LENGTH:         '500ms' # time for detecting a click on the rotary encoders; default 350ms sometimes missed

  DURATION_DISPLAY_DIM_DOWN:        '60s'
  DISPLAY_BACKLIGHT_FULL_ON:       '100%'
  DISPLAY_BACKLIGHT_DIM:            '30%'
  DISPLAY_BACKLIGHT_OFF:             '0%'

  DISPLAY_COLOR_BACKGROUND:    'COLOR_CSS_BLACK'
  DISPLAY_COLOR_CLOCK:         'COLOR_CSS_WHITE'
  DISPLAY_COLOR_DIM_BAR:       'COLOR_CSS_RED'
  DISPLAY_COLOR_DIM_BOX:       'COLOR_CSS_CHARTREUSE'
  DISPLAY_COLOR_DIM_TEXT:      'COLOR_CSS_CYAN'
  DISPLAY_COLOR_ENVIRON:       'COLOR_CSS_CHARTREUSE'
  DISPLAY_COLOR_NODENAME:      'COLOR_CSS_BLUE'
  DISPLAY_COLOR_TIMEOUT:       'COLOR_CSS_YELLOW'

  DISPLAY_FONT_CLOCK:          'font_huge'
  DISPLAY_FONT_DIMMER:         'font_medium'
  DISPLAY_FONT_ENVIRON:        'font_small'
  DISPLAY_FONT_NODENAME:       'font_small'
  DISPLAY_FONT_TIMEOUT:        'font_medium'

  # Sparkfun ESP32 Thing pin mapping from schematic names
  PIN_ROTARY_A_ENC_1:      GPIO36
  PIN_ROTARY_A_ENC_2:      GPIO37
  PIN_ROTARY_A_SW:         GPIO38
  PIN_ROTARY_B_ENC_1:      GPIO39
  PIN_ROTARY_B_ENC_2:      GPIO34
  PIN_ROTARY_B_SW:         GPIO35
  PIN_TOUCH_IRQ:           GPIO0
  PIN_ROTARY_A_LED:        GPIO12
  PIN_ROTARY_B_LED:        GPIO13
  PIN_VIDEO_CS:            GPIO14
  PIN_SD_CS:               GPIO15
  PIN_PIR_A_LED:           GPIO16
  PIN_PIR_B_LED:           GPIO17
  PIN_SPI_CLOCK:           GPIO18
  PIN_SPI_MISO:            GPIO19
  PIN_PIR_B_DATA:          GPIO2
  PIN_TOUCH_CS:            GPIO21
  PIN_SPI_MOSI:            GPIO23
  PIN_PIR_A_DATA:          GPIO25
  PIN_DATA_COMMAND:        GPIO26
  PIN_VIDEO_RESET:         GPIO27  # negated
  PIN_BACKLIGHT:           GPIO32
  PIN_PWM_A:               GPIO33
  PIN_PWM_B:               GPIO4
  PIN_I2C_SDA:             GPIO1
  PIN_I2C_SCL:             GPIO3
  PIN_ONBOARD_LED:         GPIO5   # blue led
  PIN_BUZZER:              GPIO22

esphome:
  name: $node_name
  platform: ESP32
  board: esp32thing
  on_boot:
    - script.execute: S00_away_all_boats

wifi:
  ssid:      !secret wifi_ssid
  id:        !secret wifi_ssid
  password:  !secret wifi_password
  power_save_mode: high
  fast_connect: off
  manual_ip:
    static_ip: ${wifi_static_ip}
    gateway: !secret wifi_gateway
    subnet:  !secret wifi_subnet
    dns1:    !secret wifi_dns1
    dns2:    !secret wifi_dns2
  # This is the fallback AP if the node
  # can't connect to the above wifi network.
  ap:
    ssid: "${node_name} Fallback AP"
    password: !secret wifi_ap_password

captive_portal:
web_server:
  port: 80
  auth:
    username: !secret web_username
    password: !secret web_password

logger:
  level: ${log_level}
api:
  password: !secret api_password
  reboot_timeout: 0s
ota:
  password: !secret ota_password
switch:
  - platform: restart
    name: "${node_name} Reboot"

packages:
#  colors: !include ../color/COLOR_CSS

   black:  !include ../color/COLOR_CSS_BLACK
   white:  !include ../color/COLOR_CSS_WHITE
   red:    !include ../color/COLOR_CSS_RED
   cyan:   !include ../color/COLOR_CSS_CYAN
   blue:   !include ../color/COLOR_CSS_BLUE
   yellow: !include ../color/COLOR_CSS_YELLOW 
   chartr: !include ../color/COLOR_CSS_CHARTREUSE

# Using this time component to count down to automatic shut-off. The time
# won't be precise because of loop overhead, but it will be close enough
# for the thing it keeps track of.
time:
  - platform: sntp
    id: id_time
    on_time:
      - seconds: '*'
        then:
          - globals.set:
              id: TimeoutSecondsRemaining
              value: !lambda |-
                int v = id(TimeoutSecondsRemaining) - 1;
                if (v < 0) {
                  // TODO actually turn lights off instead of resetting
                  v = $DURATION_LIGHTS_STAY_ON;
                  ESP_LOGD("main", "Resetting countdown to %d", $DURATION_LIGHTS_STAY_ON);
                }
                return v;

globals:
  # This is the value of the dimmer, expressed as a percentage. 0 == off, 100 == full on
  - id: LightLevelCurrent
    type: int
    #restore_value: true
    initial_value: '53'
  - id: LightLevelTarget
    type: int
    initial_value: '53'

  # For determining if the rotary encoder went up or down
  - {id: G_rotary_A_previous_value, type: int, initial_value: '0'}
  - {id: G_rotary_B_previous_value, type: int, initial_value: '0'}

  - id: TimeoutSecondsRemaining
    type: int
    initial_value: $DURATION_LIGHTS_STAY_ON

  - {id: GS05_active_activity_text,  type: "const char*"}
  - {id: GS06_passive_activity_text, type: "const char*"}
  - {id: GS07_activity_text,         type: "const char*"}
  # these timestamps are in seconds since ESP32 startup
  - {id: G_time_of_recent_active_activity,  type: double}
  - {id: G_time_of_recent_passive_activity, type: double}
  - {id: G_time_of_recent_activity,         type: double}

spi: {clk_pin: $PIN_SPI_CLOCK, mosi_pin: $PIN_SPI_MOSI, miso_pin: $PIN_SPI_MISO}
i2c: {sda: $PIN_I2C_SDA, scl: $PIN_I2C_SCL, scan: true, id: id_i2c_bus}
output:
  # The MOSFET gates for the "A" and "B" sets of lights
  - {platform: ledc, id: id_pwm_a, frequency: 19531, pin: {number: $PIN_PWM_A, mode: pulldown}}
  - {platform: ledc, id: id_pwm_b, frequency: 19531, pin: {number: $PIN_PWM_B, mode: pulldown}}
  # The backlight for the display. Controlled as PWM for dimming
  - {platform: ledc, id: id_backlight, frequency: 19531, pin: {number: $PIN_BACKLIGHT}}
  # TODO: Only using the ESP32 on-board LED for diagnostic purposes
  - {platform: ledc, id: id_onboard_led, frequency: 19531, pin: {number: $PIN_ONBOARD_LED}}

sensor:

    # We used uptime as an easy way to examine relative
    # times. We don't really care about the absolute values.
    # Update interval is "never" because we force update when we need it.
  - platform: uptime
    id: device_uptime
    name: "${node_name} Uptime"
    update_interval: never
    internal: true

  - platform: bme280
    id: id_bme280
    address: 0x76
    update_interval: 300s
    temperature: {name: "$node_name Temp",      id: id_temperature, oversampling: 16x, device_class: "temperature"}
    pressure:    {name: "$node_name AirPr",     id: id_pressure,    oversampling: 16x, device_class: "pressure"}
    humidity:    {name: "$node_name RHumidity", id: id_humidity,    oversampling: 16x, device_class: "humidity"}

  - platform: rotary_encoder
    name: "Rotary A"
    id: id_rotary_a
    pin_a: $PIN_ROTARY_A_ENC_1
    pin_b: $PIN_ROTARY_A_ENC_2
    resolution: 1
    on_value:
      then:
      - globals.set: {id: GS05_active_activity_text, value: '"Rotary A rotate on_value"'}
      - script.execute: S05_react_to_active_activity
      - globals.set:
          id: LightLevelCurrent
          value: !lambda |-
            int steps = x - id(G_rotary_A_previous_value);
            int newValue = steps * $ROTARY_STEP_SCALER + id(LightLevelCurrent);
            if (newValue < 0) newValue = 0;
            if (newValue > 100) newValue = 100;
            ESP_LOGD("main", "Rotary A new value %d", newValue);
            return newValue;

      - globals.set: {id: G_rotary_A_previous_value, value: !lambda "return id(id_rotary_a).state;"}

  - platform: rotary_encoder
    name: "Rotary B"
    id: id_rotary_b
    pin_a: $PIN_ROTARY_B_ENC_1
    pin_b: $PIN_ROTARY_B_ENC_2
    resolution: 1
    on_clockwise:
      then:
      - globals.set: {id: GS05_active_activity_text, value: '"Rotary B rotate on_value"'}
      - script.execute: S05_react_to_active_activity
      - globals.set:
          id: LightLevelCurrent
          value: !lambda |-
            //int steps = x - id(G_rotary_B_previous_value);
            int steps = 1;
            int newValue = steps * $ROTARY_STEP_SCALER + id(LightLevelCurrent);
            if (newValue < 0) newValue = 0;
            if (newValue > 100) newValue = 100;
            ESP_LOGD("main", "Rotary B new value %d", newValue);
            return newValue;

      - globals.set: {id: G_rotary_B_previous_value, value: !lambda "return id(id_rotary_b).state;"}

binary_sensor:
  # Rotary switches mode: INPUT_PULLUP ... no internal pull-up; use external resistor
  - platform: gpio
    name: "Rotary A Switch"
    id: id_rotary_a_switch
    pin: {number: $PIN_ROTARY_A_SW, inverted: true}
    on_click:
      max_length: $ROTARY_CLICK_MAX_LENGTH
      then:
        - component.update: id_display
        - globals.set: {id: GS05_active_activity_text, value: '"Rotary A switch on_click"'}
        - script.execute: S05_react_to_active_activity

  - platform: gpio
    name: "Rotary B Switch"
    id: id_rotary_b_switch
    pin: {number: $PIN_ROTARY_B_SW, inverted: true}
    on_click:
      max_length: $ROTARY_CLICK_MAX_LENGTH
      then:
        - component.update: id_display
        - globals.set: {id: GS05_active_activity_text, value: '"Rotary B switch on_click"'}
        - script.execute: S05_react_to_active_activity

  - platform: gpio
    name: "PIR A"
    id: id_pir_a
    pin: $PIN_PIR_A_DATA
    device_class: motion
    on_state:
      then:
      - globals.set: {id: GS06_passive_activity_text, value: '"PIR A rotate on_state"'}
      - script.execute: S06_react_to_passive_activity

  - platform: gpio
    name: "PIR B"
    id: id_pir_b
    pin: $PIN_PIR_B_DATA
    device_class: motion
    on_state:
      then:
      - globals.set: {id: GS06_passive_activity_text, value: '"PIR A rotate on_state"'}
      - script.execute: S06_react_to_passive_activity

switch:
  - {platform: gpio, name: "Rotary A LED", id: id_rotary_a_led, pin: {number: $PIN_ROTARY_A_LED}}
  - {platform: gpio, name: "Rotary B LED", id: id_rotary_b_led, pin: {number: $PIN_ROTARY_B_LED}}
  - {platform: gpio, name: "PIR A LED",    id: id_pir_a_led,    pin: {number: $PIN_PIR_A_LED}}
  - {platform: gpio, name: "PIR B LED",    id: id_pir_b_led,    pin: {number: $PIN_PIR_B_LED}}

font:
  - {file: '../font/dejavu/DejaVuSans.ttf',          id: font_tiny,   size:  8}
  - {file: '../font/dejavu/DejaVuSans.ttf',          id: font_small,  size: 14}
  - {file: '../font/dejavu/DejaVuSans.ttf',          id: font_medium, size: 36}
  - {file: '../font/dejavu/DejaVuSans.ttf',          id: font_large,  size: 60}
  - {file: '../font/dejavu/DejaVuSansMono-Bold.ttf', id: font_huge,   size: 96}

display:
  - platform: ili9341
    model: tft_2.4
    id: id_display
    cs_pin:    $PIN_VIDEO_CS
    dc_pin:    $PIN_DATA_COMMAND
    reset_pin: $PIN_VIDEO_RESET
    update_interval: 0.5s
    rotation: 90
    # tiny small medium large huge
    lambda: |-

      //////////////////////////////////////// SIZES //////////////////////////////////////
      static int max_width = it.get_width() - 1;
      static int center_width = (it.get_width() / 2) - 1;
      static int max_height = it.get_height() - 1;
      static int center_height = (it.get_height() / 2) - 1;
      //it.filled_rectangle(0,0, max_width,max_height, $DISPLAY_COLOR_BACKGROUND);

      // compute heights of all the font sizes, assuming "0" is a full-height glyph
      static int dummy_width = -1, dummy_zero_x, dummy_zero_y;
      static int clock_height, timeout_height, env_height, dimmer_height, nodename_height;
      if (dummy_width == -1) {
        it.get_text_bounds(0, 0, "0", id($DISPLAY_FONT_CLOCK),    TextAlign::TOP_LEFT, &dummy_zero_x, &dummy_zero_y, &dummy_width, &clock_height);
        it.get_text_bounds(0, 0, "0", id($DISPLAY_FONT_DIMMER),   TextAlign::TOP_LEFT, &dummy_zero_x, &dummy_zero_y, &dummy_width, &dimmer_height);
        it.get_text_bounds(0, 0, "0", id($DISPLAY_FONT_ENVIRON),  TextAlign::TOP_LEFT, &dummy_zero_x, &dummy_zero_y, &dummy_width, &env_height);
        it.get_text_bounds(0, 0, "0", id($DISPLAY_FONT_NODENAME), TextAlign::TOP_LEFT, &dummy_zero_x, &dummy_zero_y, &dummy_width, &nodename_height);
        it.get_text_bounds(0, 0, "0", id($DISPLAY_FONT_TIMEOUT),  TextAlign::TOP_LEFT, &dummy_zero_x, &dummy_zero_y, &dummy_width, &timeout_height);
      }

      //////////////////////////////////////// ENVIRONMENT //////////////////////////////////////
      float temp_c = id(id_temperature).state;
      float temp_f = (temp_c * (9.0/5.0)) + 32.0;
      float press_hpa = id(id_pressure).state;
      float press_inhg = (press_hpa / 100.0) * 2.953;
      float hum_pct = id(id_humidity).state;
      int pos_y = 0;
      it.printf(max_width, pos_y,
        id($DISPLAY_FONT_NODENAME), $DISPLAY_COLOR_NODENAME, TextAlign::TOP_RIGHT,
        "%s", "$node_name");
      it.printf(0, pos_y,
        id($DISPLAY_FONT_ENVIRON), $DISPLAY_COLOR_ENVIRON, TextAlign::TOP_LEFT,
        "rH: %.0f%%", hum_pct);
      pos_y += env_height;
      it.printf(0, pos_y,
        id($DISPLAY_FONT_ENVIRON), $DISPLAY_COLOR_ENVIRON, TextAlign::TOP_LEFT,
        " T: %.1f°C", temp_c);
      it.printf(max_width, pos_y,
        id($DISPLAY_FONT_ENVIRON), $DISPLAY_COLOR_ENVIRON, TextAlign::TOP_RIGHT,
        "%.1f°F", temp_f);
      pos_y += env_height;
      it.printf(0, pos_y,
        id($DISPLAY_FONT_ENVIRON), $DISPLAY_COLOR_ENVIRON, TextAlign::TOP_LEFT,
        "Pr: %.0f", press_hpa);
      it.printf(max_width, pos_y,
        id($DISPLAY_FONT_ENVIRON), $DISPLAY_COLOR_ENVIRON, TextAlign::TOP_RIGHT,
        "%.2f\"", press_inhg);

      //////////////////////////////////////// TIMEOUT //////////////////////////////////////
      static bool timeout_on = false;
      static bool need_warn_countdown = true;
      int remaining = id(TimeoutSecondsRemaining);
      // start blinking the countdown in the final period
      bool in_final_period = remaining <= $DURATION_COUNTDOWN_TO_LIGHTS_OFF;
      if (remaining <= $DURATION_COUNTDOWN_TO_LIGHTS_OFF) {
        if (need_warn_countdown) {
          ESP_LOGD("main", "Final countdown at %d", remaining);
          need_warn_countdown = false;
        }
      } else {
        need_warn_countdown = true;
      }
      if (!in_final_period  ||  timeout_on) {
        int mins = remaining  / 60;
        int secs = remaining  % 60;
        Color color = $DISPLAY_COLOR_TIMEOUT;
        if (in_final_period) color = COLOR_CSS_RED;
        it.printf(center_width, 0, id($DISPLAY_FONT_TIMEOUT), color, TextAlign::TOP_CENTER, "%d:%02d", mins, secs);
      }
      // seconds since boot, for debug purposes
      //id(device_uptime).update();
      //it.printf(center_width, timeout_height+2, id($DISPLAY_FONT_ENVIRON), $DISPLAY_COLOR_ENVIRON, TextAlign::TOP_CENTER, "%.1f", id(device_uptime).state);
      timeout_on = !timeout_on;

      //////////////////////////////////////// CLOCK //////////////////////////////////////
      // blinks the ":" in the clock
      static bool clock_blink_on = true;
      if (id(id_time).now().is_valid()) {
        const char *clock_format = (clock_blink_on ? "%H:%M" : "%H %M");
        it.strftime(center_width, center_height, id($DISPLAY_FONT_CLOCK), $DISPLAY_COLOR_CLOCK, TextAlign::CENTER, clock_format, id(id_time).now());
      } else {
        const char *invalid = (clock_blink_on ? "-.:.-" : ".-:-.");
        it.printf(center_width, center_height, id($DISPLAY_FONT_CLOCK), $DISPLAY_COLOR_CLOCK, TextAlign::CENTER, invalid);
      }
      clock_blink_on = !clock_blink_on;

      //////////////////////////////////////// DIMMER //////////////////////////////////////
      static int box_height = dimmer_height;
      static int box_top_line = max_height-box_height;
      static int box_bottom_line = max_height;
      static int box_left = 0, box_right = max_width, box_full_width = box_right - box_left;
      static int bar_height = box_height - 2;
      static int bar_top_line = box_top_line + 1;
      static int bar_bottom_line = box_bottom_line - 1;
      static int bar_left = box_left + 1, bar_right = box_right - 1, bar_full_width = bar_right - bar_left;
      int pos_x;
      // the bar
      int light_level = id(LightLevelCurrent);
      pos_x = (light_level/100.0) * bar_full_width;
      it.filled_rectangle(bar_left, bar_top_line, pos_x, bar_height, $DISPLAY_COLOR_DIM_BAR);
      // the box
      it.rectangle(box_left, box_top_line, box_full_width, box_height, $DISPLAY_COLOR_DIM_BOX);
      for (int ii=1; ii<10; ++ii) {
        pos_x = (ii/10.0) * max_width;
        it.line(pos_x, box_top_line, pos_x, box_bottom_line, $DISPLAY_COLOR_DIM_BOX);
      }
      pos_y = max_height;
      it.printf(center_width, pos_y,
        id($DISPLAY_FONT_DIMMER), $DISPLAY_COLOR_DIM_TEXT, TextAlign::BOTTOM_CENTER,
        "Dimmer: %d%%", light_level);

      // feedback indicators
      if (id(id_rotary_a_switch).state) {
        it.printf(0, pos_y,
        id($DISPLAY_FONT_DIMMER), $DISPLAY_COLOR_DIM_BOX, TextAlign::BOTTOM_LEFT,
        "A");
      //} else {
      //  it.printf(0, pos_y,
      //  id($DISPLAY_FONT_DIMMER), $DISPLAY_COLOR_DIM_BAR, TextAlign::BOTTOM_LEFT,
      //  "A");
      }
      // feedback indicators
      if (id(id_rotary_b_switch).state) {
        it.printf(0, pos_y,
        id($DISPLAY_FONT_DIMMER), $DISPLAY_COLOR_DIM_BOX, TextAlign::BOTTOM_LEFT,
        "B");
      //} else {
      //  it.printf(0, pos_y,
      //  id($DISPLAY_FONT_DIMMER), $DISPLAY_COLOR_DIM_BAR, TextAlign::BOTTOM_LEFT,
      //  "B");
      }

script:
  # Things to do at boot time
  - id: S00_away_all_boats
    then:
      - logger.log: {level: DEBUG, format: "S00_away_all_boats"}
      - globals.set: {id: GS06_passive_activity_text, value: '"device boot-up"'}
      - script.execute: S06_react_to_passive_activity
      - script.execute: S02_display_full
      - sensor.rotary_encoder.set_value: {id: id_rotary_a, value: '25'}
      - sensor.rotary_encoder.set_value: {id: id_rotary_b, value: '25'}
# only for troubleshooting/feedback
#      - script.execute: S01_display_dim
#      - delay: 5s
#      - script.execute: S02_display_full
      - script.execute: S04_time_watcher

  # Sets the display to the "dim" backlight level
  - id: S01_display_dim
    then:
      - logger.log: {level: DEBUG, format: "S01_display_dim"}
      - output.set_level: {id: id_backlight, level: $DISPLAY_BACKLIGHT_DIM}
  # Sets the display to the "full" backlight level
  - id: S02_display_full
    then:
      - logger.log: {level: DEBUG, format: "S02_display_full"}
      - output.set_level: {id: id_backlight, level: $DISPLAY_BACKLIGHT_FULL_ON}

  # This script is watching the clock to act on these time-based items:
  #   -- dim display from full after a period of idleness
  #   -- alter display during final countdown period
  #   -- turn lights off after a period of idleness
  - id: S04_time_watcher
    then:
      - logger.log: {level: INFO, format: "S04_time_watcher"}

  # Common action when an "active" activity is noticed
  - id: S05_react_to_active_activity
    then:
      - component.update: device_uptime
      - globals.set: {id: G_time_of_recent_active_activity, value: !lambda "return id(device_uptime).state;"}
      - logger.log: {level: DEBUG, format: "S05_react_to_active_activity '%s' at %.1fs", args: [id(GS05_active_activity_text), id(G_time_of_recent_active_activity)]}
      - globals.set: {id: GS07_activity_text, value: id(GS05_active_activity_text)}
      - script.execute: S07_react_to_any_activity
  # Common action when a "passive" activity is noticed
  - id: S06_react_to_passive_activity
    then:
      - component.update: device_uptime
      - globals.set: {id: G_time_of_recent_passive_activity, value: !lambda "return id(device_uptime).state;"}
      - logger.log: {level: DEBUG, format: "S06_react_to_passive_activity '%s' at %.1fs", args: [id(GS06_passive_activity_text), id(G_time_of_recent_passive_activity)]}
      - globals.set: {id: GS07_activity_text, value: id(GS05_active_activity_text)}
      - script.execute: S07_react_to_any_activity
  # Common action when a any activity is noticed, active or passive
  - id: S07_react_to_any_activity
    then:
#      - logger.log: {level: DEBUG, format: "S07_react_to_any_activity: %s", args: [id(GS07_activity_text)]}
      - component.update: device_uptime
      - globals.set: {id: G_time_of_recent_activity, value: !lambda "return id(device_uptime).state;"}
      - logger.log: {level: DEBUG, format: "S07_react_to_any_activity at %.1fs", args: [id(G_time_of_recent_activity)]}
