#:########################################################################################:#
# TITLE: LOUNGE ENVIRONMENT SENSOR
#:########################################################################################:#
# REPO:
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-loungeenvironment.yaml
#:########################################################################################:#
# VERSIONS:
# V1.5 2026-03-22 Updated yaml to zorruno layout V1.1; fixed units to °C; added
#                 device_class/state_class; retained MQTT publishing and all existing logic
# V1.4 2026-03-22 Updated yaml to zorruno layout V1.1; added MQTT publishing for
#                 temperature, humidity and pressure; retained all existing sensor logic
# V1.3 2026-03-22 Added MQTT publishing for temperature, humidity and pressure readings
# V1.2 2026-02-16 Pressure tendency now 5-state over 3 hours + rate/hour, 1h removed
# V1.1 2026-02-16 Added derived min/max sensors and pressure trend text sensors
# V1.0 2026-02-16 Initial Version
#:########################################################################################:#
# HARDWARE:
# - ESP32-C3 SuperMini
# - BME280 (I2C)
# - SDA = GPIO1
# - SCL = GPIO3
# - I2C address = 0x76
#:########################################################################################:#
# OPERATION NOTES:
# - Reads temperature, humidity and pressure from the BME280 every 20 seconds
# - Publishes each fresh reading to local MQTT status topics:
#     - ${mqtt_local_status_topic}/temperature
#     - ${mqtt_local_status_topic}/humidity
#     - ${mqtt_local_status_topic}/pressure
# - Tracks:
#     - Lowest Temp Overnight
#     - Highest Temp Today (Daytime)
#     - Highest/Lowest Temp This Month
#     - Highest/Lowest Temp Last Month
#     - Highest/Lowest Temp Last 24 Hours
#     - Lowest Humidity Overnight
#     - Highest Humidity Today (Daytime)
#     - Highest/Lowest Humidity This Month
#     - Highest/Lowest Humidity Last Month
#     - Highest/Lowest Humidity Last 24 Hours
# - Pressure direction uses a 3-hour tendency:
#     - rising rapidly
#     - rising
#     - steady
#     - falling
#     - falling rapidly
# - Pressure rate per hour is calculated from the 3-hour pressure delta
#:########################################################################################:#
# OFFLINE NOTES:
# a) HA offline (network + MQTT online)
#    - Device continues reading sensors and publishing local MQTT status topics.
# b) MQTT offline (or HA/MQTT offline)
#    - Device continues reading sensors and updating local derived values internally.
# c) Entire WiFi/Network offline
#    - Accurate time is needed for daily/monthly min/max periods and pressure trend timing.
#    - If SNTP is unavailable, the fallback clock can drift and can be reset by power
#      cycling at 12pm (midday).
#:########################################################################################:#

substitutions:
  # Device Naming
  device_name: "esp-loungeenvironment"
  friendly_name: "Lounge Environment"
  description_comment: "Lounge Environment :: ESP32-C3 SuperMini + BME280 (Layout V1.1)"
  device_area: "Lounge"

  # Project Naming
  project_name: "ESP32-C3 SuperMini.BME280"
  project_version: "v1.5"

  # Passwords
  api_key: !secret esp-api_key
  ota_pass: !secret esp-ota_pass
  static_ip_address: !secret esp-loungeenvironment_ip

  # Layout V1.1 Required
  current_ip_address: ${static_ip_address}

  # MQTT Local Status
  mqtt_local_status_main_topic: !secret mqtt_status_main_topic
  mqtt_device_name: "lounge-environment"
  mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}"

  # General Settings
  log_level: "WARN"
  update_interval: "60s"

  # I2C Settings
  i2c_sda_pin: "GPIO1"
  i2c_scl_pin: "GPIO3"
  i2c_frequency: "100kHz"
  bmp_address: "0x76"

  # Pressure tendency thresholds (hPa over 3 hours)
  # - falling rapidly: <= -6.0 hPa / 3h
  # - falling:        <= -1.0 hPa / 3h
  # - steady:         between -1.0 and +1.0 hPa / 3h
  # - rising:         >= +1.0 hPa / 3h
  # - rising rapidly: >= +6.0 hPa / 3h
  pressure_3h_steady_band: "1.0"
  pressure_3h_rapid_threshold: "6.0"

##########################################################################################
# PACKAGES
##########################################################################################
packages:
  common_wifi: !include
    file: common/network_common.yaml
    vars:
      local_device_name: "${device_name}"
      local_static_ip_address: "${static_ip_address}"
      local_current_ip_address: "${current_ip_address}"
      local_ota_pass: "${ota_pass}"

  common_api: !include
    file: common/api_common.yaml
    vars:
      local_api_key: "${api_key}"

  common_mqtt: !include
    file: common/mqtt_common.yaml
    vars:
      local_device_name: "${device_name}"

  # Provides:
  # - time id: sntp_time
  # - global: current_mins (minutes since midnight, fallback supported)
  # - text sensors: time_text, time_sync, device_last_restart
  common_sntp: !include common/sntp_common.yaml

  diag_basic: !include common/include_basic_diag_sensors.yaml
  diag_more: !include common/include_more_diag_sensors.yaml

##########################################################################################
# ESPHOME
##########################################################################################
esphome:
  name: "${device_name}"
  friendly_name: "${friendly_name}"
  comment: "${description_comment}"
  area: "${device_area}"
  name_add_mac_suffix: false
  min_version: 2026.1.0
  project:
    name: "${project_name}"
    version: "${project_version}"

##########################################################################################
# PLATFORM
##########################################################################################
esp32:
  board: esp32-c3-devkitm-1
  variant: ESP32C3
  framework:
    type: esp-idf
  cpu_frequency: 80MHz

preferences:
  flash_write_interval: 5min

mdns:
  disabled: false

##########################################################################################
# LOGGING
##########################################################################################
logger:
  level: "${log_level}"
  baud_rate: 0

##########################################################################################
# I2C
##########################################################################################
i2c:
  sda: ${i2c_sda_pin}
  scl: ${i2c_scl_pin}
  scan: true
  frequency: ${i2c_frequency}

##########################################################################################
# GLOBALS (tracking min/max across time windows)
##########################################################################################
globals:
  # Last completed overnight window (23:00 -> 07:00) min temperature
  - id: g_min_temp_today_overnight
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Working overnight min temperature for the current 23:00 -> 07:00 window
  - id: g_work_min_temp_overnight
    type: float
    restore_value: no
    initial_value: "NAN"

  # Last completed daytime window (07:00 -> 23:00) max temperature
  - id: g_max_temp_today_daytime
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Working daytime max temperature for the current 07:00 -> 23:00 window
  - id: g_work_max_temp_daytime
    type: float
    restore_value: no
    initial_value: "NAN"

  # Last completed overnight window (23:00 -> 07:00) min humidity
  - id: g_min_humidity_today_overnight
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Working overnight min humidity for the current 23:00 -> 07:00 window
  - id: g_work_min_humidity_overnight
    type: float
    restore_value: no
    initial_value: "NAN"

  # Last completed daytime window (07:00 -> 23:00) max humidity
  - id: g_max_humidity_today_daytime
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Working daytime max humidity for the current 07:00 -> 23:00 window
  - id: g_work_max_humidity_daytime
    type: float
    restore_value: no
    initial_value: "NAN"

  # Current month max temperature
  - id: g_max_temp_this_month
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Last month max temperature
  - id: g_max_temp_last_month
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Current month min temperature
  - id: g_min_temp_this_month
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Last month min temperature
  - id: g_min_temp_last_month
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Current month max humidity
  - id: g_max_humidity_this_month
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Last month max humidity
  - id: g_max_humidity_last_month
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Current month min humidity
  - id: g_min_humidity_this_month
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Last month min humidity
  - id: g_min_humidity_last_month
    type: float
    restore_value: yes
    initial_value: "NAN"

  # Track current calendar month/year so we can roll over month stats
  - id: g_current_month
    type: int
    restore_value: yes
    initial_value: "0"

  - id: g_current_year
    type: int
    restore_value: yes
    initial_value: "0"

  # Hour-bucket rolling 24h min/max temperature
  - id: g_temp_hour_min
    type: std::array<float, 24>
    restore_value: no
    initial_value: "{NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN}"

  - id: g_temp_hour_max
    type: std::array<float, 24>
    restore_value: no
    initial_value: "{NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN}"

  - id: g_temp_hour_ts
    type: std::array<uint32_t, 24>
    restore_value: no
    initial_value: "{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}"

  # Hour-bucket rolling 24h min/max humidity
  - id: g_hum_hour_min
    type: std::array<float, 24>
    restore_value: no
    initial_value: "{NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN}"

  - id: g_hum_hour_max
    type: std::array<float, 24>
    restore_value: no
    initial_value: "{NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN}"

  - id: g_hum_hour_ts
    type: std::array<uint32_t, 24>
    restore_value: no
    initial_value: "{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}"

  # Pressure sampling (every 5 minutes) for trend calculation
  - id: g_pressure_samples
    type: std::array<float, 40>
    restore_value: no
    initial_value: "{NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN,NAN}"

  - id: g_pressure_ts
    type: std::array<uint32_t, 40>
    restore_value: no
    initial_value: "{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}"

  - id: g_pressure_idx
    type: int
    restore_value: no
    initial_value: "0"

##########################################################################################
# SENSORS
##########################################################################################
sensor:
  - platform: bme280_i2c
    address: ${bmp_address}
    update_interval: 20s

    temperature:
      name: "${friendly_name} Temperature"
      id: lounge_temperature
      unit_of_measurement: "°C"
      device_class: temperature
      state_class: measurement
      accuracy_decimals: 2
      filters:
        - offset: 0.27
      on_value:
        then:
          # Publish current temperature to local MQTT status topic
          - mqtt.publish:
              topic: "${mqtt_local_status_topic}/temperature"
              payload: !lambda |-
                char buffer[32];
                snprintf(buffer, sizeof(buffer), "%.2f", x);
                return std::string(buffer);

          - lambda: |-
              auto now = id(sntp_time).now();
              if (!now.is_valid()) return;

              const int hour = now.hour;
              const uint32_t now_ts = now.timestamp;

              {
                int idx = hour;
                if (idx < 0) idx = 0;
                if (idx > 23) idx = 23;
                id(g_temp_hour_ts)[idx] = now_ts;

                float v = x;
                if (isnan(id(g_temp_hour_min)[idx]) || v < id(g_temp_hour_min)[idx]) id(g_temp_hour_min)[idx] = v;
                if (isnan(id(g_temp_hour_max)[idx]) || v > id(g_temp_hour_max)[idx]) id(g_temp_hour_max)[idx] = v;
              }

              const int mins = id(current_mins);
              const bool in_overnight = (mins >= 1380) || (mins < 420);

              if (in_overnight) {
                if (isnan(id(g_work_min_temp_overnight)) || x < id(g_work_min_temp_overnight)) {
                  id(g_work_min_temp_overnight) = x;
                }
              } else {
                if (isnan(id(g_work_max_temp_daytime)) || x > id(g_work_max_temp_daytime)) {
                  id(g_work_max_temp_daytime) = x;
                }
              }

              if (isnan(id(g_min_temp_this_month)) || x < id(g_min_temp_this_month)) id(g_min_temp_this_month) = x;
              if (isnan(id(g_max_temp_this_month)) || x > id(g_max_temp_this_month)) id(g_max_temp_this_month) = x;

    pressure:
      name: "${friendly_name} Pressure"
      id: lounge_pressure
      unit_of_measurement: "hPa"
      device_class: pressure
      state_class: measurement
      accuracy_decimals: 2
      on_value:
        then:
          # Publish current pressure to local MQTT status topic
          - mqtt.publish:
              topic: "${mqtt_local_status_topic}/pressure"
              payload: !lambda |-
                char buffer[32];
                snprintf(buffer, sizeof(buffer), "%.2f", x);
                return std::string(buffer);

    humidity:
      name: "${friendly_name} Humidity"
      id: lounge_humidity
      unit_of_measurement: "%"
      device_class: humidity
      state_class: measurement
      accuracy_decimals: 1
      on_value:
        then:
          # Publish current humidity to local MQTT status topic
          - mqtt.publish:
              topic: "${mqtt_local_status_topic}/humidity"
              payload: !lambda |-
                char buffer[32];
                snprintf(buffer, sizeof(buffer), "%.1f", x);
                return std::string(buffer);

          - lambda: |-
              auto now = id(sntp_time).now();
              if (!now.is_valid()) return;

              const int hour = now.hour;
              const uint32_t now_ts = now.timestamp;

              {
                int idx = hour;
                if (idx < 0) idx = 0;
                if (idx > 23) idx = 23;
                id(g_hum_hour_ts)[idx] = now_ts;

                float v = x;
                if (isnan(id(g_hum_hour_min)[idx]) || v < id(g_hum_hour_min)[idx]) id(g_hum_hour_min)[idx] = v;
                if (isnan(id(g_hum_hour_max)[idx]) || v > id(g_hum_hour_max)[idx]) id(g_hum_hour_max)[idx] = v;
              }

              const int mins = id(current_mins);
              const bool in_overnight = (mins >= 1380) || (mins < 420);

              if (in_overnight) {
                if (isnan(id(g_work_min_humidity_overnight)) || x < id(g_work_min_humidity_overnight)) {
                  id(g_work_min_humidity_overnight) = x;
                }
              } else {
                if (isnan(id(g_work_max_humidity_daytime)) || x > id(g_work_max_humidity_daytime)) {
                  id(g_work_max_humidity_daytime) = x;
                }
              }

              if (isnan(id(g_min_humidity_this_month)) || x < id(g_min_humidity_this_month)) id(g_min_humidity_this_month) = x;
              if (isnan(id(g_max_humidity_this_month)) || x > id(g_max_humidity_this_month)) id(g_max_humidity_this_month) = x;

  # min_temp_today_overnight (this is the min temp, from 11pm to 7am for the last daily period)
  - platform: template
    name: "Lowest Temp Overnight"
    id: min_temp_today_overnight
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return id(g_min_temp_today_overnight);

  # max_temp_today_daytime (this is the max temp, from 7am to 11pm for the last daily period)
  - platform: template
    name: "Highest Temp Today (Daytime)"
    id: max_temp_today_daytime
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return id(g_max_temp_today_daytime);

  # max_temp_this_month (max for this current calendar month)
  - platform: template
    name: "Highest Temp This Month"
    id: max_temp_this_month
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return id(g_max_temp_this_month);

  # max_temp_last_month (max for this last calendar month)
  - platform: template
    name: "Highest Temp Last Month"
    id: max_temp_last_month
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return id(g_max_temp_last_month);

  # min_temp_this_month (min for this current calendar month)
  - platform: template
    name: "Lowest Temp This Month"
    id: min_temp_this_month
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return id(g_min_temp_this_month);

  # min_temp_last_month (min for this last calendar month)
  - platform: template
    name: "Lowest Temp Last Month"
    id: min_temp_last_month
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      return id(g_min_temp_last_month);

  # min_temp_today (min temp for last rolling 24 hours)
  - platform: template
    name: "Lowest Temp Last 24 Hours"
    id: min_temp_today
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      auto now = id(sntp_time).now();
      if (!now.is_valid()) return NAN;
      const uint32_t now_ts = now.timestamp;

      float m = NAN;
      for (int i = 0; i < 24; i++) {
        if (id(g_temp_hour_ts)[i] == 0) continue;
        if ((now_ts - id(g_temp_hour_ts)[i]) > 86400) continue;
        float v = id(g_temp_hour_min)[i];
        if (isnan(v)) continue;
        if (isnan(m) || v < m) m = v;
      }
      return m;

  # max_temp_today (max temp for last rolling 24 hours)
  - platform: template
    name: "Highest Temp Last 24 Hours"
    id: max_temp_today
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      auto now = id(sntp_time).now();
      if (!now.is_valid()) return NAN;
      const uint32_t now_ts = now.timestamp;

      float m = NAN;
      for (int i = 0; i < 24; i++) {
        if (id(g_temp_hour_ts)[i] == 0) continue;
        if ((now_ts - id(g_temp_hour_ts)[i]) > 86400) continue;
        float v = id(g_temp_hour_max)[i];
        if (isnan(v)) continue;
        if (isnan(m) || v > m) m = v;
      }
      return m;

  # min_humidity_today_overnight (this is the min humidity, from 11pm to 7am for the last daily period)
  - platform: template
    name: "Lowest Humidity Overnight"
    id: min_humidity_today_overnight
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      return id(g_min_humidity_today_overnight);

  # max_humidity_today_daytime (this is the max humidity, from 7am to 11pm for the last daily period)
  - platform: template
    name: "Highest Humidity Today (Daytime)"
    id: max_humidity_today_daytime
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      return id(g_max_humidity_today_daytime);

  # max_humidity_this_month (max for this current calendar month)
  - platform: template
    name: "Highest Humidity This Month"
    id: max_humidity_this_month
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      return id(g_max_humidity_this_month);

  # max_humidity_last_month (max for this last calendar month)
  - platform: template
    name: "Highest Humidity Last Month"
    id: max_humidity_last_month
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      return id(g_max_humidity_last_month);

  # min_humidity_this_month (min for this current calendar month)
  - platform: template
    name: "Lowest Humidity This Month"
    id: min_humidity_this_month
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      return id(g_min_humidity_this_month);

  # min_humidity_last_month (min for this last calendar month)
  - platform: template
    name: "Lowest Humidity Last Month"
    id: min_humidity_last_month
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      return id(g_min_humidity_last_month);

  # min_humidity_today (min humidity for last rolling 24 hours)
  - platform: template
    name: "Lowest Humidity Last 24 Hours"
    id: min_humidity_today
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      auto now = id(sntp_time).now();
      if (!now.is_valid()) return NAN;
      const uint32_t now_ts = now.timestamp;

      float m = NAN;
      for (int i = 0; i < 24; i++) {
        if (id(g_hum_hour_ts)[i] == 0) continue;
        if ((now_ts - id(g_hum_hour_ts)[i]) > 86400) continue;
        float v = id(g_hum_hour_min)[i];
        if (isnan(v)) continue;
        if (isnan(m) || v < m) m = v;
      }
      return m;

  # max_humidity_today (max humidity for last rolling 24 hours)
  - platform: template
    name: "Highest Humidity Last 24 Hours"
    id: max_humidity_today
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 1
    update_interval: 60s
    lambda: |-
      auto now = id(sntp_time).now();
      if (!now.is_valid()) return NAN;
      const uint32_t now_ts = now.timestamp;

      float m = NAN;
      for (int i = 0; i < 24; i++) {
        if (id(g_hum_hour_ts)[i] == 0) continue;
        if ((now_ts - id(g_hum_hour_ts)[i]) > 86400) continue;
        float v = id(g_hum_hour_max)[i];
        if (isnan(v)) continue;
        if (isnan(m) || v > m) m = v;
      }
      return m;

  # Pressure Rate Per Hour (3h) (hPa/hour based on delta over the last ~3 hours)
  - platform: template
    name: "Pressure Rate Per Hour (3h)"
    id: pressure_rate_per_hour_3h
    unit_of_measurement: "hPa/h"
    state_class: measurement
    accuracy_decimals: 2
    update_interval: 60s
    lambda: |-
      auto now = id(sntp_time).now();
      if (!now.is_valid()) return NAN;

      const uint32_t now_ts = now.timestamp;
      const uint32_t target_ts = now_ts - 10800;

      float past = NAN;
      uint32_t best_dt = 0xFFFFFFFF;

      for (int i = 0; i < 40; i++) {
        uint32_t ts = id(g_pressure_ts)[i];
        if (ts == 0) continue;
        uint32_t dt = (ts > target_ts) ? (ts - target_ts) : (target_ts - ts);
        if (dt < best_dt) {
          best_dt = dt;
          past = id(g_pressure_samples)[i];
        }
      }

      float cur = id(lounge_pressure).state;
      if (isnan(cur) || isnan(past)) return NAN;

      float delta_3h = cur - past;
      return delta_3h / 3.0f;

##########################################################################################
# TEXT SENSORS (pressure tendency)
##########################################################################################
text_sensor:
  # Pressure Direction, Last 3 Hours (5-state tendency over the last ~3 hours)
  - platform: template
    name: "Pressure Direction, Last 3 Hours"
    id: pressure_rising_or_falling_3_hours
    update_interval: 60s
    lambda: |-
      auto now = id(sntp_time).now();
      if (!now.is_valid()) return std::string("unknown");

      const uint32_t now_ts = now.timestamp;
      const uint32_t target_ts = now_ts - 10800;

      float past = NAN;
      uint32_t best_dt = 0xFFFFFFFF;

      for (int i = 0; i < 40; i++) {
        uint32_t ts = id(g_pressure_ts)[i];
        if (ts == 0) continue;
        uint32_t dt = (ts > target_ts) ? (ts - target_ts) : (target_ts - ts);
        if (dt < best_dt) {
          best_dt = dt;
          past = id(g_pressure_samples)[i];
        }
      }

      float cur = id(lounge_pressure).state;
      if (isnan(cur) || isnan(past)) return std::string("unknown");

      float delta_3h = cur - past;

      float steady_band = atof("${pressure_3h_steady_band}");
      float rapid_thr = atof("${pressure_3h_rapid_threshold}");

      if (delta_3h >= rapid_thr) return std::string("rising rapidly");
      if (delta_3h >= steady_band) return std::string("rising");
      if (delta_3h <= -rapid_thr) return std::string("falling rapidly");
      if (delta_3h <= -steady_band) return std::string("falling");
      return std::string("steady");

##########################################################################################
# INTERVALS (rollover overnight/daytime periods, month rollover, and pressure sampling)
##########################################################################################
interval:
  # Sample pressure every 5 minutes for trend calculations
  - interval: 5min
    then:
      - lambda: |-
          auto now = id(sntp_time).now();
          if (!now.is_valid()) return;

          float p = id(lounge_pressure).state;
          if (isnan(p)) return;

          const uint32_t ts = now.timestamp;

          int idx = id(g_pressure_idx);
          if (idx < 0) idx = 0;
          if (idx > 39) idx = 0;

          id(g_pressure_samples)[idx] = p;
          id(g_pressure_ts)[idx] = ts;

          idx++;
          if (idx >= 40) idx = 0;
          id(g_pressure_idx) = idx;

  # Check period boundaries once a minute (uses current_mins, works during fallback)
  - interval: 60s
    then:
      - lambda: |-
          const int mins = id(current_mins);
          const bool at_0700 = (mins == 420);
          const bool at_2300 = (mins == 1380);

          if (at_0700) {
            id(g_min_temp_today_overnight) = id(g_work_min_temp_overnight);
            id(g_work_min_temp_overnight) = NAN;

            id(g_min_humidity_today_overnight) = id(g_work_min_humidity_overnight);
            id(g_work_min_humidity_overnight) = NAN;
          }

          if (at_2300) {
            id(g_max_temp_today_daytime) = id(g_work_max_temp_daytime);
            id(g_work_max_temp_daytime) = NAN;

            id(g_max_humidity_today_daytime) = id(g_work_max_humidity_daytime);
            id(g_work_max_humidity_daytime) = NAN;
          }

          auto now = id(sntp_time).now();
          if (!now.is_valid()) return;

          const int mon = now.month;
          const int yr = now.year;

          if (id(g_current_month) == 0 || id(g_current_year) == 0) {
            id(g_current_month) = mon;
            id(g_current_year) = yr;
          }

          if (mon != id(g_current_month) || yr != id(g_current_year)) {
            id(g_max_temp_last_month) = id(g_max_temp_this_month);
            id(g_min_temp_last_month) = id(g_min_temp_this_month);
            id(g_max_humidity_last_month) = id(g_max_humidity_this_month);
            id(g_min_humidity_last_month) = id(g_min_humidity_this_month);

            id(g_max_temp_this_month) = NAN;
            id(g_min_temp_this_month) = NAN;
            id(g_max_humidity_this_month) = NAN;
            id(g_min_humidity_this_month) = NAN;

            id(g_current_month) = mon;
            id(g_current_year) = yr;
          }