diff --git a/.ha_run.lock b/.ha_run.lock index c872fe4..4646251 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 67, "version": 1, "ha_version": "2026.2.0", "start_ts": 1770279860.4199212} \ No newline at end of file +{"pid": 67, "version": 1, "ha_version": "2026.2.2", "start_ts": 1771127640.2174048} \ No newline at end of file diff --git a/esphome/esp-loungeenvironment.yaml b/esphome/esp-loungeenvironment.yaml new file mode 100644 index 0000000..2d9b11c --- /dev/null +++ b/esphome/esp-loungeenvironment.yaml @@ -0,0 +1,737 @@ +########################################################################################## +########################################################################################## +# LOUNGE ENVIRONMENT SENSOR +# +# ESP32-C3 SuperMini + BME280 (I2C) +# +# 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 +########################################################################################## +########################################################################################## + +substitutions: + # Device Naming + device_name: "esp-loungeenvironment" + friendly_name: "Lounge Environment" + description_comment: "Lounge Environment :: ESP32-C3 SuperMini + BME280" + device_area: "Lounge" + + # Project Naming + project_name: "ESP32-C3 SuperMini.BME280" + project_version: "v1.2" + + # Passwords + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-loungeenvironment_ip + + # 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_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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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" + on_value: + then: + - lambda: |- + auto now = id(sntp_time).now(); + // We only need hour buckets and month stats if we have valid SNTP. + if (!now.is_valid()) return; + + const int hour = now.hour; + const uint32_t now_ts = now.timestamp; + + // Update hourly bucket (rolling 24h) + { + 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; + } + + // Daily window tracking uses current_mins so it still works during fallback. + // Overnight: 23:00 -> 07:00 + // Daytime: 07:00 -> 23:00 + 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; + } + } + + // Month stats + 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 + + humidity: + name: "${friendly_name} Humidity" + id: lounge_humidity + on_value: + then: + - lambda: |- + auto now = id(sntp_time).now(); + if (!now.is_valid()) return; + + const int hour = now.hour; + const uint32_t now_ts = now.timestamp; + + // Update hourly bucket (rolling 24h) + { + 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; + } + + // Daily window tracking uses current_mins so it still works during fallback. + 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; + } + } + + // Month stats + 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" + 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" + 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" + 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" + 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" + 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" + 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" + 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" + 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: "%" + 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: "%" + 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: "%" + 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: "%" + 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: "%" + 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: "%" + 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: "%" + 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: "%" + 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" + 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; // 3h + + 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) + # Outputs exactly one of: + # - rising rapidly + # - rising + # - steady + # - falling + # - falling rapidly + - 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; // 3h + + 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); + + // Roll overnight window at 07:00 + 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; + } + + // Roll daytime window at 23:00 + 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; + } + + // Month rollover requires real date/time (SNTP valid) + 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)) { + // Copy current month stats to last month + 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); + + // Reset current month stats + 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; + } \ No newline at end of file diff --git a/esphome/esp-mainbathroomlights.yaml b/esphome/esp-mainbathroomlights.yaml index f61f978..98eee43 100644 --- a/esphome/esp-mainbathroomlights.yaml +++ b/esphome/esp-mainbathroomlights.yaml @@ -20,7 +20,7 @@ substitutions: # Device Naming device_name: "esp-mainbathroomlights" - friendly_name: "Main Bathroom Lightswitch (3) 2" + friendly_name: "Main Bathroom Lightswitch (3)" description_comment: "BK7231N - Main Bathroom Lightswitch using a Zemismart KS-811 Triple Push Button. Main Lights (1), Shower Lights (2), Cabinet Lights (3)" device_area: "Main Bathroom" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. @@ -159,7 +159,7 @@ binary_sensor: name: "Button 1: ${switch_1_name}" on_press: - delay: 50ms - - switch.toggle: Relay_1 + - switch.toggle: Relay_2 #(FIX shoud be relay 1) - platform: gpio pin: @@ -183,7 +183,7 @@ binary_sensor: name: "Button 3: ${switch_3_name}" on_press: - delay: 50ms - - switch.turn_off: Relay_1 + #- switch.turn_off: Relay_1 - switch.turn_off: Relay_2 - switch.turn_off: Relay_3 @@ -211,4 +211,23 @@ switch: name: "Relay 3: ${switch_3_name}" pin: P15 #GPIO15 id: Relay_3 - restore_mode: ${relay_restore_mode} \ No newline at end of file + restore_mode: ${relay_restore_mode} + +########################################################################################## +# TEMPLATE COMPONENT +# +########################################################################################## +# Virtual group switch for Home Assistant + - platform: template + name: "All Main Bathroom Lights" + id: all_main_bathroom_lights + optimistic: false + restore_mode: RESTORE_DEFAULT_OFF + lambda: |- + return id(Relay_1).state || id(Relay_2).state || id(Relay_3).state; + turn_on_action: + - switch.turn_on: Relay_2 + turn_off_action: + #- switch.turn_off: Relay_1 + - switch.turn_off: Relay_2 + - switch.turn_off: Relay_3 \ No newline at end of file diff --git a/packages/climate_aircon_controls.yaml b/packages/climate_aircon_controls.yaml deleted file mode 100644 index 9082e20..0000000 --- a/packages/climate_aircon_controls.yaml +++ /dev/null @@ -1,95 +0,0 @@ -# input_select for dropdown menu to select the timer duration or cancel -input_select: - master_bedroom_climate_timer_duration: - name: Master Bedroom Aircon Sleep - options: - - "1 hour" - - "2 hours" - - "3 hours" - - "Cancel sleep timer" - initial: "Cancel sleep timer" - icon: mdi:timer-outline - -# Single timer for managing the countdown -timer: - master_bedroom_climate_timer: - name: Master Bedroom Aircon Timer - duration: "00:00:00" # Initial duration; will be set dynamically - -# Automation to start or cancel the timer based on the dropdown selection -automation: - - alias: "Start or Cancel Master Bedroom Climate Timer" - trigger: - - platform: state - entity_id: input_select.master_bedroom_climate_timer_duration - action: - - service: timer.cancel - entity_id: timer.master_bedroom_climate_timer - - - choose: - - conditions: - - condition: state - entity_id: input_select.master_bedroom_climate_timer_duration - state: "1 hour" - sequence: - - service: timer.start - data: - entity_id: timer.master_bedroom_climate_timer - duration: "01:00:00" - - - conditions: - - condition: state - entity_id: input_select.master_bedroom_climate_timer_duration - state: "1 minute" - sequence: - - service: timer.start - data: - entity_id: timer.master_bedroom_climate_timer - duration: "00:01:00" - - - conditions: - - condition: state - entity_id: input_select.master_bedroom_climate_timer_duration - state: "2 hours" - sequence: - - service: timer.start - data: - entity_id: timer.master_bedroom_climate_timer - duration: "02:00:00" - - - conditions: - - condition: state - entity_id: input_select.master_bedroom_climate_timer_duration - state: "3 hours" - sequence: - - service: timer.start - data: - entity_id: timer.master_bedroom_climate_timer - duration: "03:00:00" - - - conditions: - - condition: state - entity_id: input_select.master_bedroom_climate_timer_duration - state: "Cancel sleep timer" - sequence: - - service: timer.cancel - entity_id: timer.master_bedroom_climate_timer - - service: climate.turn_off - target: - entity_id: climate.master_bedroom - - # Automation to turn off the climate entity when the timer finishes - - alias: "Turn Off Master Bedroom Climate on Timer Completion" - trigger: - - platform: event - event_type: timer.finished - event_data: - entity_id: timer.master_bedroom_climate_timer - action: - - service: climate.turn_off - target: - entity_id: climate.master_bedroom - - service: input_select.select_option - data: - entity_id: input_select.master_bedroom_climate_timer_duration - option: "Cancel sleep timer" # Reset to the initial value diff --git a/packages/climate_aircon_controls.yaml.old20260213 b/packages/climate_aircon_controls.yaml.old20260213 new file mode 100644 index 0000000..e6dffff --- /dev/null +++ b/packages/climate_aircon_controls.yaml.old20260213 @@ -0,0 +1,120 @@ +# ------------------------------ +# TIMER HELPER (define in YAML so it definitely exists) +# ------------------------------ +timer: + master_bedroom_climate_timer: + name: Master Bedroom Climate Timer + duration: "00:00:00" + +# ------------------------------ +# DROPDOWN (sleep timer) +# ------------------------------ +input_select: + master_bedroom_climate_timer_duration: + name: Master Bedroom Aircon Sleep + options: + - "1 hour" + - "2 hours" + - "3 hours" + - "6 hours" + - "Cancel timer" + initial: "Cancel timer" + icon: mdi:timer-outline + +# ------------------------------ +# LIVE REMAINING (updates immediately on selection, and every minute) +# ------------------------------ +template: + - trigger: + # Update once per minute (suits hh:mm display) + - platform: time_pattern + minutes: "/1" + + # Update when timer STATE changes (idle/active/paused) + - platform: state + entity_id: timer.master_bedroom_climate_timer + + # Update immediately when user changes the dropdown + - platform: state + entity_id: input_select.master_bedroom_climate_timer_duration + + sensor: + - name: "Master Bedroom Climate Timer Remaining" + unique_id: master_bedroom_climate_timer_remaining + icon: mdi:timer + state: >- + {% set st = states('timer.master_bedroom_climate_timer') | lower %} + {% if st == 'idle' %} + 0:00 + {% else %} + {% set fa = state_attr('timer.master_bedroom_climate_timer', 'finishes_at') %} + {% if fa %} + {% set end = as_datetime(fa) %} + {% set sec = (end - now()).total_seconds() | int(0) %} + {% if sec < 0 %}{% set sec = 0 %}{% endif %} + {% set hh = (sec // 3600) | int %} + {% set mm = ((sec % 3600) // 60) | int %} + {{ hh }}:{{ '%02d' | format(mm) }} + {% else %} + 0:00 + {% endif %} + {% endif %} + +# ------------------------------ +# AUTOMATION: start/cancel timer when dropdown changes +# ------------------------------ +automation: + - alias: "Start or Cancel Master Bedroom Climate Timer" + mode: restart + trigger: + - platform: state + entity_id: input_select.master_bedroom_climate_timer_duration + + variables: + opt: "{{ states('input_select.master_bedroom_climate_timer_duration') }}" + dur_map: + "1 hour": "01:00:00" + "2 hours": "02:00:00" + "3 hours": "03:00:00" + "6 hours": "06:00:00" + + action: + - choose: + - conditions: + - condition: template + value_template: "{{ opt in dur_map }}" + sequence: + - service: timer.cancel + target: + entity_id: timer.master_bedroom_climate_timer + - delay: "00:00:01" + - service: timer.start + target: + entity_id: timer.master_bedroom_climate_timer + data: + duration: "{{ dur_map[opt] }}" + + - conditions: + - condition: state + entity_id: input_select.master_bedroom_climate_timer_duration + state: "Cancel timer" + sequence: + - service: timer.cancel + target: + entity_id: timer.master_bedroom_climate_timer + + - alias: "Turn Off Master Bedroom Climate on Timer Completion" + trigger: + - platform: event + event_type: timer.finished + event_data: + entity_id: timer.master_bedroom_climate_timer + action: + - service: climate.turn_off + target: + entity_id: climate.master_bedroom + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_climate_timer_duration + data: + option: "Cancel timer" diff --git a/packages/climate_master_bedroom_dehum.yaml b/packages/climate_master_bedroom_dehum.yaml.old20260213 similarity index 100% rename from packages/climate_master_bedroom_dehum.yaml rename to packages/climate_master_bedroom_dehum.yaml.old20260213 diff --git a/packages/dashboard_helpers.yaml b/packages/dashboard_helpers.yaml new file mode 100644 index 0000000..93a482e --- /dev/null +++ b/packages/dashboard_helpers.yaml @@ -0,0 +1,17 @@ +input_boolean: + moon_phase_percent_overlay: + name: Moon phase percent overlay + icon: mdi:percent + +script: + moon_phase_percent_overlay_10s: + alias: Moon phase percent overlay (10s) + mode: restart + sequence: + - service: input_boolean.turn_on + target: + entity_id: input_boolean.moon_phase_percent_overlay + - delay: "00:00:10" + - service: input_boolean.turn_off + target: + entity_id: input_boolean.moon_phase_percent_overlay diff --git a/packages/downstairs_flat_controls.yaml b/packages/downstairs_flat_controls.yaml new file mode 100644 index 0000000..04b3079 --- /dev/null +++ b/packages/downstairs_flat_controls.yaml @@ -0,0 +1,115 @@ +input_boolean: + downstairs_flat_occupied: + name: Downstairs Guest Occupied + icon: mdi:briefcase-plus-outline + +script: + downstairs_light_toggle_lounge_main: + alias: Downstairs Light Toggle - Lounge Main Lights + mode: queued + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + - service: switch.toggle + target: + entity_id: switch.esp_downstloungemain_relay_1_main_lights + + downstairs_light_toggle_lounge_back_wall: + alias: Downstairs Light Toggle - Lounge Back Wall Lights + mode: queued + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + - service: switch.toggle + target: + entity_id: switch.esp_downstloungemain_relay_2_back_wall_lights + + downstairs_light_toggle_main_bedroom_main: + alias: Downstairs Light Toggle - Main Bedroom Main Lights + mode: queued + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + - service: switch.toggle + target: + entity_id: switch.esp_downstmasterbedrmlights_relay_1_main_lights + + downstairs_light_toggle_small_bedroom_main: + alias: Downstairs Light Toggle - Small Bedroom Room Lights + mode: queued + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + - service: switch.toggle + target: + entity_id: switch.esp_downstbedrm2lights_relay_1_main_lights + + downstairs_light_toggle_kitchen: + alias: Downstairs Light Toggle - Kitchen Lights + mode: queued + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + - service: switch.toggle + target: + entity_id: switch.esp_downstkitchlights_relay_2_kitchen_light + + downstairs_light_toggle_dining: + alias: Downstairs Light Toggle - Dining Lights + mode: queued + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + - service: switch.toggle + target: + entity_id: switch.esp_downstkitchlights_relay_1_dining_light + + downstairs_light_toggle_bathroom_main: + alias: Downstairs Light Toggle - Bathroom Main Lights + mode: queued + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + - service: switch.toggle + target: + entity_id: switch.esp_downstbathswitch_relay_1_main_lights + + downstairs_light_toggle_bathroom_cabinet: + alias: Downstairs Light Toggle - Bathroom Cabinet Lights + mode: queued + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + - service: switch.toggle + target: + entity_id: switch.esp_downstbathswitch_relay_2_cabinet_light + + downstairs_light_toggle_all_group: + alias: Downstairs Lights - All On/Off + mode: single + sequence: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + + - choose: + - conditions: + - condition: state + entity_id: group.downstairs_flat_lights + state: "on" + sequence: + - service: homeassistant.turn_off + target: + entity_id: group.downstairs_flat_lights + default: + - service: homeassistant.turn_on + target: + entity_id: group.downstairs_flat_lights diff --git a/packages/ev_charging.yaml b/packages/ev_charging.yaml index 8af80d7..a586c50 100644 --- a/packages/ev_charging.yaml +++ b/packages/ev_charging.yaml @@ -62,3 +62,37 @@ automation: "1hr top-up now":"boost", "Charge now until full":"now-full", "Stop charging now":"stop" } %} {% set dta = trigger.to_state.state %} {{ mapping[dta] }} ' + +template: + - trigger: + # Update at least every 10 seconds (and also on HA start) + - platform: time_pattern + seconds: "/10" + - platform: homeassistant + event: start + binary_sensor: + - name: "32A EV Charger Charging" + unique_id: ev_charger_32a_charging + icon: mdi:car-electric + state: >- + {{ (states('sensor.32a_ev_wallcharger_power') | float(0)) > 0.1 }} + delay_on: + seconds: 60 + delay_off: + seconds: 60 + + - name: "16A EV Charger Charging" + unique_id: ev_charger_16a_charging + icon: mdi:car-electric + state: >- + {{ (states('sensor.16a_ev_wallcharger_power') | float(0)) > 0.1 }} + delay_on: + seconds: 60 + delay_off: + seconds: 60 + + - binary_sensor: + - name: 32A EV Charger Charging UI + unique_id: ev_charger_charging_ui + state: "{{ is_state('sensor.32a_ev_charger_charging', 'on') }}" + icon: mdi:car-electric diff --git a/packages/foxhole_controls.yaml b/packages/foxhole_controls.yaml deleted file mode 100644 index 93a0c3f..0000000 --- a/packages/foxhole_controls.yaml +++ /dev/null @@ -1,4 +0,0 @@ -input_boolean: - foxhole_occupied: - name: Foxhole Guest Occupied - icon: mdi:briefcase-plus-outline diff --git a/packages/heat_pump_helpers.yaml.old20260213 b/packages/heat_pump_helpers.yaml.old20260213 new file mode 100644 index 0000000..947b147 --- /dev/null +++ b/packages/heat_pump_helpers.yaml.old20260213 @@ -0,0 +1,309 @@ +########################################################################################## +# heat_pump_helpers.yaml +# +# Panasonic Comfort Cloud (panasonic_cc) helpers for Heat/Cool control + presets. +# +# Purpose: +# - Provide "optimistic" UI feedback for cloud/slow mode changes +# - Provide one-tap scripts for Off/Heat/Cool/Dehumid +# - Provide a 3-step "Profile" cycler (Normal/Boost/Quiet) using preset_mode: +# * preset_mode: boost -> Powerful ON (we label this "Boost") +# * preset_mode: eco -> Quiet ON (we label this "Quiet") +# * preset_mode: none -> Powerful OFF + Quiet OFF (we label this "Normal") +# +# Notes: +# - Your climate entity supports preset_modes: [none, eco, boost] +# - The switch.lounge_ai_eco entity appears to be read-only (cannot be controlled), +# so "Eco" and "Eco/Quiet" profiles have been removed. +# - When turning the heat pump OFF, we also force preset_mode to "none" so that +# Boost/Quiet are cleared. +########################################################################################## + +input_select: + # Used by your dashboard buttons to "light up" instantly (optimistic state). + # When set to something other than "none", the UI can treat that as "pending". + master_bedroom_hvac_requested_mode: + name: Master Bedroom HVAC Requested Mode + options: + - "none" + - "off" + - "heat" + - "cool" + - "dry" + initial: "none" + + # Heat pump profile selector shown on the dashboard and cycled with one tap. + master_bedroom_heat_pump_profile: + name: Master Bedroom Heat Pump Profile + options: + - Normal + - Boost + - Quiet + initial: Normal + +script: + ######################################################################################## + # One-tap HVAC mode scripts + # These set the optimistic helper first, then call the real climate services. + # + # IMPORTANT: + # - Off also clears Boost/Quiet by forcing preset_mode to "none". + ######################################################################################## + + master_bedroom_hvac_off: + alias: Master Bedroom HVAC - Off + mode: restart + sequence: + # Optimistic UI: show "Off" immediately + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "off" + + # Turn the unit off (more reliable than set_hvac_mode off on many cloud integrations) + - service: climate.turn_off + target: + entity_id: climate.master_bedroom + + # Clear Panasonic preset flags: none = Boost OFF + Quiet OFF + - service: climate.set_preset_mode + target: + entity_id: climate.master_bedroom + data: + preset_mode: "none" + + master_bedroom_hvac_heat: + alias: Master Bedroom HVAC - Heat + mode: restart + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "heat" + + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_hvac_mode + target: + entity_id: climate.master_bedroom + data: + hvac_mode: "heat" + + master_bedroom_hvac_cool: + alias: Master Bedroom HVAC - Cool + mode: restart + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "cool" + + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_hvac_mode + target: + entity_id: climate.master_bedroom + data: + hvac_mode: "cool" + + master_bedroom_hvac_dry: + alias: Master Bedroom HVAC - Dehumid + mode: restart + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "dry" + + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_hvac_mode + target: + entity_id: climate.master_bedroom + data: + hvac_mode: "dry" + + ######################################################################################## + # Profile logic: Apply + Cycle + # + # Profile definitions (Panasonic CC / panasonic_cc): + # - Normal -> preset_mode none (Boost OFF, Quiet OFF) + # - Boost -> preset_mode boost (Powerful ON) + # - Quiet -> preset_mode eco (Quiet ON) + # + # The profile selector is kept in-sync with the actual preset_mode via automation. + ######################################################################################## + + master_bedroom_heat_pump_profile_apply: + alias: Master Bedroom Heat Pump - Apply Profile + mode: restart + fields: + profile: + description: Profile name (Normal, Boost, Quiet) + example: Normal + sequence: + - variables: + p: "{{ profile | default(states('input_select.master_bedroom_heat_pump_profile')) }}" + preset_for_profile: >- + {% if p == 'Normal' %}none + {% elif p == 'Boost' %}boost + {% elif p == 'Quiet' %}eco + {% else %}none + {% endif %} + + # Optional: ensure the unit is on when applying a profile. + # Comment out if you want profile selection while the unit is off. + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + # Set preset explicitly every time so Boost/Quiet are mutually exclusive + - service: climate.set_preset_mode + target: + entity_id: climate.master_bedroom + data: + preset_mode: "{{ preset_for_profile }}" + + master_bedroom_heat_pump_profile_next: + alias: Master Bedroom Heat Pump - Next Profile + mode: restart + sequence: + - variables: + cur: "{{ states('input_select.master_bedroom_heat_pump_profile') }}" + nxt: >- + {% if cur == 'Normal' %}Boost + {% elif cur == 'Boost' %}Quiet + {% else %}Normal + {% endif %} + + # Store the new selection (UI) + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_heat_pump_profile + data: + option: "{{ nxt }}" + + # Apply it to the heat pump + - service: script.turn_on + target: + entity_id: script.master_bedroom_heat_pump_profile_apply + data: + variables: + profile: "{{ nxt }}" + +automation: + ######################################################################################## + # Clear "requested mode" once the climate entity catches up, so the UI reflects reality. + # This prevents two mode buttons appearing lit due to stale cloud attributes. + ######################################################################################## + - id: master_bedroom_hvac_clear_requested_mode + alias: Master Bedroom HVAC - Clear Requested Mode + mode: restart + trigger: + - platform: state + entity_id: climate.master_bedroom + + - platform: state + entity_id: input_select.master_bedroom_hvac_requested_mode + to: + - "off" + - "heat" + - "cool" + - "dry" + + condition: + - condition: template + value_template: > + {{ states('input_select.master_bedroom_hvac_requested_mode') != 'none' }} + + action: + # Small delay to allow the cloud integration to reflect the change + - delay: "00:00:03" + + - choose: + - conditions: + - condition: template + value_template: > + {% set req = states('input_select.master_bedroom_hvac_requested_mode') %} + {% set mode = states('climate.master_bedroom') %} + {% set action = (state_attr('climate.master_bedroom','hvac_action') or '') %} + {{ (req == 'off' and (mode == 'off' or action == 'off')) + or (req in ['heat','cool','dry'] and mode == req) }} + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "none" + + default: + # Failsafe: if the cloud entity is slow or ambiguous, clear after a while anyway + - delay: "00:00:25" + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "none" + + ######################################################################################## + # Sync the Profile selector from actual device preset_mode (including changes made in + # the Panasonic app). This keeps the UI truthful. + # + # Mapping: + # - preset boost -> Boost + # - preset eco -> Quiet + # - preset none -> Normal + ######################################################################################## + - id: master_bedroom_heat_pump_profile_sync_from_entities + alias: Master Bedroom Heat Pump - Sync Profile From Entities + mode: restart + trigger: + - platform: state + entity_id: climate.master_bedroom + action: + - delay: "00:00:02" + - variables: + preset: "{{ state_attr('climate.master_bedroom', 'preset_mode') or 'none' }}" + new_profile: >- + {% if preset == 'boost' %} + Boost + {% elif preset == 'eco' %} + Quiet + {% else %} + Normal + {% endif %} + - choose: + - conditions: + - condition: template + value_template: "{{ states('input_select.master_bedroom_heat_pump_profile') != new_profile }}" + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_heat_pump_profile + data: + option: "{{ new_profile }}" + +template: + - sensor: + - name: Master Bedroom Heat Pump Profile Tile + unique_id: master_bedroom_heat_pump_profile_tile + state: "{{ states('input_select.master_bedroom_heat_pump_profile') }}" + icon: > + {% set p = states('input_select.master_bedroom_heat_pump_profile') %} + {% if p == 'Boost' %} + mdi:rocket-launch + {% elif p == 'Quiet' %} + mdi:leaf + {% else %} + mdi:gauge + {% endif %} diff --git a/packages/heat_pump_master_bedroom_helpers.yaml b/packages/heat_pump_master_bedroom_helpers.yaml new file mode 100644 index 0000000..0ce47a5 --- /dev/null +++ b/packages/heat_pump_master_bedroom_helpers.yaml @@ -0,0 +1,480 @@ +########################################################################################## +# heat_pump_master_bedroom_helpers.yaml +# +# Master Bedroom Panasonic Comfort Cloud Heat Pump Helpers +# +# Combines: +# - Setpoint helpers (optimistic cloud control) +# - HVAC mode helpers (Off / Heat / Cool / Dry) +# - Profile logic (Normal / Boost / Quiet) +# - Sleep timer system +# - Sync automations +# +# Designed for: +# climate.master_bedroom (Panasonic Comfort Cloud) +# +########################################################################################## + +########################################################################################## +# TIMER (Sleep Timer Entity) +########################################################################################## + +timer: + master_bedroom_climate_timer: + name: Master Bedroom Climate Timer + duration: "00:00:00" + +########################################################################################## +# INPUT HELPERS +########################################################################################## + +input_select: + # Sleep timer dropdown + master_bedroom_climate_timer_duration: + name: Master Bedroom Aircon Sleep + options: + - "1 hour" + - "2 hours" + - "3 hours" + - "6 hours" + - "Cancel timer" + initial: "Cancel timer" + icon: mdi:timer-outline + + # Optimistic requested HVAC mode (used by dashboard buttons) + master_bedroom_hvac_requested_mode: + name: Master Bedroom HVAC Requested Mode + options: + - "none" + - "off" + - "heat" + - "cool" + - "dry" + initial: "none" + + # Heat pump profile selector + master_bedroom_heat_pump_profile: + name: Master Bedroom Heat Pump Profile + options: + - Normal + - Boost + - Quiet + initial: Normal + +input_number: + # Optimistic UI setpoint helper + master_bedroom_setpoint_ui: + name: Master Bedroom Setpoint UI + min: 10 + max: 30 + step: 0.5 + mode: box + unit_of_measurement: "°C" + icon: mdi:thermometer + +########################################################################################## +# TEMPLATE SENSORS +########################################################################################## + +template: + ############################################################################ + # Sleep Timer Remaining (HH:MM display) + ############################################################################ + - trigger: + - platform: time_pattern + minutes: "/1" + + - platform: state + entity_id: + - timer.master_bedroom_climate_timer + - input_select.master_bedroom_climate_timer_duration + + sensor: + - name: "Master Bedroom Climate Timer Remaining" + unique_id: master_bedroom_climate_timer_remaining + icon: mdi:timer + state: >- + {% set st = states('timer.master_bedroom_climate_timer') | lower %} + {% if st == 'idle' %} + 0:00 + {% else %} + {% set fa = state_attr('timer.master_bedroom_climate_timer', 'finishes_at') %} + {% if fa %} + {% set end = as_datetime(fa) %} + {% set sec = (end - now()).total_seconds() | int(0) %} + {% if sec < 0 %}{% set sec = 0 %}{% endif %} + {% set hh = (sec // 3600) | int %} + {% set mm = ((sec % 3600) // 60) | int %} + {{ hh }}:{{ '%02d' | format(mm) }} + {% else %} + 0:00 + {% endif %} + {% endif %} + + ############################################################################ + # Profile tile sensor (for dashboard icon) + ############################################################################ + - sensor: + - name: Master Bedroom Heat Pump Profile Tile + unique_id: master_bedroom_heat_pump_profile_tile + state: "{{ states('input_select.master_bedroom_heat_pump_profile') }}" + icon: > + {% set p = states('input_select.master_bedroom_heat_pump_profile') %} + {% if p == 'Boost' %} + mdi:rocket-launch + {% elif p == 'Quiet' %} + mdi:leaf + {% else %} + mdi:gauge + {% endif %} + +########################################################################################## +# SCRIPTS +########################################################################################## + +script: + ############################################################################ + # SETPOINT SYNC (cloud → UI helper) + ############################################################################ + master_bedroom_setpoint_sync_from_climate: + alias: Master Bedroom Setpoint Sync From Climate + mode: restart + sequence: + - variables: + sp: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}" + - service: input_number.set_value + target: + entity_id: input_number.master_bedroom_setpoint_ui + data: + value: "{{ sp }}" + + ############################################################################ + # SETPOINT ADJUST (Chevron buttons) + ############################################################################ + master_bedroom_setpoint_adjust: + alias: Master Bedroom Setpoint Adjust + mode: queued + fields: + step: + description: "Step in degrees C, e.g. -0.5 or 0.5" + example: -0.5 + sequence: + - condition: template + value_template: > + {% set s = states('climate.master_bedroom') | lower %} + {{ s not in ['off','unavailable','unknown','none',''] }} + + - variables: + cur_ui: "{{ states('input_number.master_bedroom_setpoint_ui') | float(0) }}" + cur_cl: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}" + cur: "{{ cur_ui if cur_ui > 0 else cur_cl }}" + raw: "{{ cur + (step | float(0)) }}" + new: "{{ [30, [10, raw] | max] | min | round(1) }}" + + - service: input_number.set_value + target: + entity_id: input_number.master_bedroom_setpoint_ui + data: + value: "{{ new }}" + + - service: climate.set_temperature + target: + entity_id: climate.master_bedroom + data: + temperature: "{{ new }}" + + ############################################################################ + # HVAC MODE SCRIPTS + ############################################################################ + + master_bedroom_hvac_off: + alias: Master Bedroom HVAC - Off + mode: restart + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "off" + + - service: climate.turn_off + target: + entity_id: climate.master_bedroom + + - service: climate.set_preset_mode + target: + entity_id: climate.master_bedroom + data: + preset_mode: "none" + + master_bedroom_hvac_heat: + alias: Master Bedroom HVAC - Heat + mode: restart + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "heat" + + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_hvac_mode + target: + entity_id: climate.master_bedroom + data: + hvac_mode: "heat" + + master_bedroom_hvac_cool: + alias: Master Bedroom HVAC - Cool + mode: restart + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "cool" + + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_hvac_mode + target: + entity_id: climate.master_bedroom + data: + hvac_mode: "cool" + + master_bedroom_hvac_dry: + alias: Master Bedroom HVAC - Dehumid + mode: restart + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "dry" + + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_hvac_mode + target: + entity_id: climate.master_bedroom + data: + hvac_mode: "dry" + + ############################################################################ + # PROFILE SCRIPTS + ############################################################################ + + master_bedroom_heat_pump_profile_apply: + alias: Master Bedroom Heat Pump - Apply Profile + mode: restart + fields: + profile: + description: Profile name (Normal, Boost, Quiet) + sequence: + - variables: + p: "{{ profile | default(states('input_select.master_bedroom_heat_pump_profile')) }}" + preset_for_profile: >- + {% if p == 'Normal' %}none + {% elif p == 'Boost' %}boost + {% elif p == 'Quiet' %}eco + {% else %}none + {% endif %} + + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_preset_mode + target: + entity_id: climate.master_bedroom + data: + preset_mode: "{{ preset_for_profile }}" + + master_bedroom_heat_pump_profile_next: + alias: Master Bedroom Heat Pump - Next Profile + mode: restart + sequence: + - variables: + cur: "{{ states('input_select.master_bedroom_heat_pump_profile') }}" + nxt: >- + {% if cur == 'Normal' %}Boost + {% elif cur == 'Boost' %}Quiet + {% else %}Normal + {% endif %} + + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_heat_pump_profile + data: + option: "{{ nxt }}" + + - service: script.turn_on + target: + entity_id: script.master_bedroom_heat_pump_profile_apply + data: + variables: + profile: "{{ nxt }}" + +########################################################################################## +# AUTOMATIONS +########################################################################################## + +automation: + ############################################################################ + # Sleep Timer Start / Cancel + ############################################################################ + - alias: "Start or Cancel Master Bedroom Climate Timer" + mode: restart + trigger: + - platform: state + entity_id: input_select.master_bedroom_climate_timer_duration + + variables: + opt: "{{ states('input_select.master_bedroom_climate_timer_duration') }}" + dur_map: + "1 hour": "01:00:00" + "2 hours": "02:00:00" + "3 hours": "03:00:00" + "6 hours": "06:00:00" + + action: + - choose: + - conditions: + - condition: template + value_template: "{{ opt in dur_map }}" + sequence: + - service: timer.cancel + target: + entity_id: timer.master_bedroom_climate_timer + - delay: "00:00:01" + - service: timer.start + target: + entity_id: timer.master_bedroom_climate_timer + data: + duration: "{{ dur_map[opt] }}" + + - conditions: + - condition: state + entity_id: input_select.master_bedroom_climate_timer_duration + state: "Cancel timer" + sequence: + - service: timer.cancel + target: + entity_id: timer.master_bedroom_climate_timer + + ############################################################################ + # Turn off heat pump when timer finishes + ############################################################################ + - alias: "Turn Off Master Bedroom Climate on Timer Completion" + trigger: + - platform: event + event_type: timer.finished + event_data: + entity_id: timer.master_bedroom_climate_timer + action: + - service: climate.turn_off + target: + entity_id: climate.master_bedroom + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_climate_timer_duration + data: + option: "Cancel timer" + + ############################################################################ + # Sync setpoint from climate → UI helper + ############################################################################ + - id: master_bedroom_setpoint_sync + alias: Master Bedroom Setpoint Sync + mode: restart + trigger: + - platform: state + entity_id: climate.master_bedroom + - platform: homeassistant + event: start + action: + - service: script.master_bedroom_setpoint_sync_from_climate + + ############################################################################ + # Clear requested HVAC mode when cloud catches up + ############################################################################ + - id: master_bedroom_hvac_clear_requested_mode + alias: Master Bedroom HVAC - Clear Requested Mode + mode: restart + trigger: + - platform: state + entity_id: climate.master_bedroom + - platform: state + entity_id: input_select.master_bedroom_hvac_requested_mode + to: ["off", "heat", "cool", "dry"] + + condition: + - condition: template + value_template: > + {{ states('input_select.master_bedroom_hvac_requested_mode') != 'none' }} + + action: + - delay: "00:00:03" + + - choose: + - conditions: + - condition: template + value_template: > + {% set req = states('input_select.master_bedroom_hvac_requested_mode') %} + {% set mode = states('climate.master_bedroom') %} + {% set action = (state_attr('climate.master_bedroom','hvac_action') or '') %} + {{ (req == 'off' and (mode == 'off' or action == 'off')) + or (req in ['heat','cool','dry'] and mode == req) }} + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "none" + + default: + - delay: "00:00:25" + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_hvac_requested_mode + data: + option: "none" + + ############################################################################ + # Sync Profile from actual preset_mode + ############################################################################ + - id: master_bedroom_heat_pump_profile_sync_from_entities + alias: Master Bedroom Heat Pump - Sync Profile From Entities + mode: restart + trigger: + - platform: state + entity_id: climate.master_bedroom + action: + - delay: "00:00:02" + - variables: + preset: "{{ state_attr('climate.master_bedroom', 'preset_mode') or 'none' }}" + new_profile: >- + {% if preset == 'boost' %} + Boost + {% elif preset == 'eco' %} + Quiet + {% else %} + Normal + {% endif %} + - choose: + - conditions: + - condition: template + value_template: "{{ states('input_select.master_bedroom_heat_pump_profile') != new_profile }}" + sequence: + - service: input_select.select_option + target: + entity_id: input_select.master_bedroom_heat_pump_profile + data: + option: "{{ new_profile }}" diff --git a/packages/heat_pump_master_bedroom_schedules.yaml b/packages/heat_pump_master_bedroom_schedules.yaml new file mode 100644 index 0000000..6b11a56 --- /dev/null +++ b/packages/heat_pump_master_bedroom_schedules.yaml @@ -0,0 +1,218 @@ +########################################################################################## +# heat_pump_master_bedroom_schedules.yaml +########################################################################################## + +########################################################################################## +# HELPERS +########################################################################################## + +input_boolean: + master_bedroom_night_power_mode: + name: Night Power Mode + icon: mdi:weather-night + + master_bedroom_9pm_on: + name: 9pm On + icon: mdi:clock-start + + master_bedroom_midnight_off: + name: Midnight Off + icon: mdi:clock-end + + master_bedroom_boost_restore_off: + name: Master Bedroom Boost Restore Off + icon: mdi:power + +########################################################################################## +# TIMERS +########################################################################################## + +timer: + master_bedroom_boost_timer: + name: Master Bedroom Boost Timer + duration: "00:00:00" + +########################################################################################## +# SCRIPTS +########################################################################################## + +script: + master_bedroom_boost_1hr: + alias: Master Bedroom Boost 1hr + mode: restart + sequence: + # Remember if the heat pump was OFF when boost started + - service: input_boolean.turn_off + target: + entity_id: input_boolean.master_bedroom_boost_restore_off + + - choose: + - conditions: + - condition: state + entity_id: climate.master_bedroom + state: "off" + sequence: + - service: input_boolean.turn_on + target: + entity_id: input_boolean.master_bedroom_boost_restore_off + + # Start boost (this will turn it on if it was off) + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_preset_mode + target: + entity_id: climate.master_bedroom + data: + preset_mode: "boost" + + - service: timer.start + target: + entity_id: timer.master_bedroom_boost_timer + data: + duration: "01:00:00" + +########################################################################################## +# AUTOMATIONS +########################################################################################## + +automation: + ########################################################################################## + # CANCEL LOGIC + ########################################################################################## + + - alias: MB Cancel Others When Night Power Enabled + trigger: + - platform: state + entity_id: input_boolean.master_bedroom_night_power_mode + to: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: + - input_boolean.master_bedroom_9pm_on + - input_boolean.master_bedroom_midnight_off + + - alias: MB Cancel Night Power If 9pm On Enabled + trigger: + - platform: state + entity_id: input_boolean.master_bedroom_9pm_on + to: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.master_bedroom_night_power_mode + + - alias: MB Cancel Night Power If Midnight Off Enabled + trigger: + - platform: state + entity_id: input_boolean.master_bedroom_midnight_off + to: "on" + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.master_bedroom_night_power_mode + + ########################################################################################## + # 9PM LOGIC + ########################################################################################## + + - alias: MB 9PM Start Logic + trigger: + - platform: time + at: "21:00:00" + action: + - choose: + - conditions: + - condition: state + entity_id: input_boolean.master_bedroom_night_power_mode + state: "on" + sequence: + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + - service: climate.set_preset_mode + target: + entity_id: climate.master_bedroom + data: + preset_mode: "boost" + + - service: timer.start + target: + entity_id: timer.master_bedroom_boost_timer + data: + duration: "00:30:00" + + - conditions: + - condition: state + entity_id: input_boolean.master_bedroom_9pm_on + state: "on" + sequence: + - service: climate.turn_on + target: + entity_id: climate.master_bedroom + + ########################################################################################## + # MIDNIGHT LOGIC + ########################################################################################## + + - alias: MB Midnight Off Logic + trigger: + - platform: time + at: "00:00:00" + action: + - choose: + - conditions: + - condition: or + conditions: + - condition: state + entity_id: input_boolean.master_bedroom_midnight_off + state: "on" + - condition: state + entity_id: input_boolean.master_bedroom_night_power_mode + state: "on" + sequence: + - service: climate.turn_off + target: + entity_id: climate.master_bedroom + + ########################################################################################## + # BOOST TIMER FINISHED + ########################################################################################## + + - alias: MB Boost Timer Finished + trigger: + - platform: event + event_type: timer.finished + event_data: + entity_id: timer.master_bedroom_boost_timer + action: + - choose: + # If boost started while the heat pump was OFF, ensure it ends OFF + - conditions: + - condition: state + entity_id: input_boolean.master_bedroom_boost_restore_off + state: "on" + sequence: + - service: climate.turn_off + target: + entity_id: climate.master_bedroom + + # Otherwise, revert to Normal (preset_mode none) and keep running + - conditions: + - condition: state + entity_id: input_boolean.master_bedroom_boost_restore_off + state: "off" + sequence: + - service: climate.set_preset_mode + target: + entity_id: climate.master_bedroom + data: + preset_mode: "none" + + # Always clear the helper + - service: input_boolean.turn_off + target: + entity_id: input_boolean.master_bedroom_boost_restore_off diff --git a/packages/heat_pump_set_point_cloud_helpers.yaml.old20260213 b/packages/heat_pump_set_point_cloud_helpers.yaml.old20260213 new file mode 100644 index 0000000..63a0ceb --- /dev/null +++ b/packages/heat_pump_set_point_cloud_helpers.yaml.old20260213 @@ -0,0 +1,76 @@ +input_number: + master_bedroom_setpoint_ui: + name: Master Bedroom Setpoint UI + min: 10 + max: 30 + step: 0.5 + mode: box + unit_of_measurement: "°C" + icon: mdi:thermometer + +script: + master_bedroom_setpoint_sync_from_climate: + alias: Master Bedroom Setpoint Sync From Climate + mode: restart + sequence: + - variables: + sp: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}" + - service: input_number.set_value + target: + entity_id: input_number.master_bedroom_setpoint_ui + data: + value: "{{ sp }}" + + master_bedroom_setpoint_adjust: + alias: Master Bedroom Setpoint Adjust + mode: queued + fields: + step: + description: "Step in degrees C, e.g. -0.5 or 0.5" + example: -0.5 + sequence: + # ───────────────────────────── + # GUARD: only block when climate is OFF/unavailable/unknown + # (Do NOT use hvac_action == idle - Panasonic cloud often reports idle) + # ───────────────────────────── + - condition: template + value_template: > + {% set s = states('climate.master_bedroom') | lower %} + {{ s not in ['off', 'unavailable', 'unknown', 'none', ''] }} + + # ───────────────────────────── + # Compute new setpoint + # Prefer UI helper if valid, otherwise use climate temp + # ───────────────────────────── + - variables: + cur_ui: "{{ states('input_number.master_bedroom_setpoint_ui') | float(0) }}" + cur_cl: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}" + cur: "{{ cur_ui if cur_ui > 0 else cur_cl }}" + raw: "{{ cur + (step | float(0)) }}" + new: "{{ [30, [10, raw] | max] | min | round(1) }}" + + # Update UI immediately + - service: input_number.set_value + target: + entity_id: input_number.master_bedroom_setpoint_ui + data: + value: "{{ new }}" + + # Send to climate (cloud may lag) + - service: climate.set_temperature + target: + entity_id: climate.master_bedroom + data: + temperature: "{{ new }}" + +automation: + - id: master_bedroom_setpoint_sync + alias: Master Bedroom Setpoint Sync + mode: restart + trigger: + - platform: state + entity_id: climate.master_bedroom + - platform: homeassistant + event: start + action: + - service: script.master_bedroom_setpoint_sync_from_climate \ No newline at end of file diff --git a/packages/main_bathroom_actions.yaml b/packages/main_bathroom_actions.yaml new file mode 100644 index 0000000..e69de29 diff --git a/packages/room_fan_control.yaml b/packages/room_fan_control.yaml index c3129b3..782f693 100644 --- a/packages/room_fan_control.yaml +++ b/packages/room_fan_control.yaml @@ -1,3 +1,11 @@ +########################################################################################## +# MASTER BEDROOM FAN (Tasmota / Sonoff iFan02) +# Improvements: +# - Numeric speed entity (0-3) reflecting actual device state via MQTT +# - Button to cycle speed 1,2,3,0 +# - Keep input_select in sync with actual FanSpeed (incl after reboots) +########################################################################################## + input_select: master_bedroom_fan_set: name: Master Bedroom Fan Speed @@ -24,21 +32,121 @@ input_select: - "Medium" - "High" +########################################################################################## +# MASTER BEDROOM FAN - Helpers +########################################################################################## +input_number: + master_bedroom_fan_speed_numeric: + name: Master Bedroom Fan Speed (Numeric) + min: 0 + max: 3 + step: 1 + mode: box + +template: + - button: + - name: Bedroom 1 Fan Speed Change + unique_id: bedroom_1_fan_speed_change + icon: mdi:fan + press: + - variables: + cur: "{{ states('input_number.master_bedroom_fan_speed_numeric') | int(0) }}" + nxt: "{{ (cur + 1) if (cur < 3) else 0 }}" + - service: mqtt.publish + data: + topic: cmnd/tasmo-ifan02-3793-bedrm1-1/FanSpeed + payload: "{{ nxt }}" + # Optimistic local update (MQTT will correct if needed) + - service: input_number.set_value + data: + entity_id: input_number.master_bedroom_fan_speed_numeric + value: "{{ nxt }}" + - service: input_select.select_option + data: + entity_id: input_select.master_bedroom_fan_set + option: >- + {% set m = {0:'Off',1:'Low',2:'Medium',3:'High'} %} + {{ m[nxt] }} + automation: + ######################################################################################## + # MASTER BEDROOM FAN - Query on HA start (fixes wrong value after reboots) + ######################################################################################## + - id: master_bedroom_fan_query_on_ha_start + initial_state: true + alias: Master Bedroom Fan - Query FanSpeed on HA Start + trigger: + - platform: homeassistant + event: start + action: + - service: mqtt.publish + data: + topic: cmnd/tasmo-ifan02-3793-bedrm1-1/FanSpeed + payload: "" + + ######################################################################################## + # MASTER BEDROOM FAN - Sync helpers from Tasmota MQTT + ######################################################################################## + - id: master_bedroom_fan_sync_from_mqtt + initial_state: true + alias: Master Bedroom Fan - Sync Helpers From MQTT FanSpeed + trigger: + - platform: mqtt + topic: stat/tasmo-ifan02-3793-bedrm1-1/RESULT + - platform: mqtt + topic: tele/tasmo-ifan02-3793-bedrm1-1/STATE + condition: + - condition: template + value_template: "{{ trigger.payload_json is defined and (trigger.payload_json.FanSpeed is defined) }}" + action: + - variables: + spd: "{{ trigger.payload_json.FanSpeed | int(0) }}" + opt: >- + {% set m = {0:'Off',1:'Low',2:'Medium',3:'High'} %} + {{ m[spd] }} + - service: input_number.set_value + data: + entity_id: input_number.master_bedroom_fan_speed_numeric + value: "{{ spd }}" + # Only update dropdown if it differs (avoid loops) + - condition: template + value_template: "{{ states('input_select.master_bedroom_fan_set') != opt }}" + - service: input_select.select_option + data: + entity_id: input_select.master_bedroom_fan_set + option: "{{ opt }}" + + ######################################################################################## + # MASTER BEDROOM FAN - Set speed from dropdown (improved) + ######################################################################################## - id: set_the_master_bedroom_fan initial_state: true alias: Set the Master Bedroom Fan trigger: - entity_id: input_select.master_bedroom_fan_set - platform: state + - platform: state + entity_id: input_select.master_bedroom_fan_set + variables: + desired: >- + {% set mapping = { 'Off':0, 'Low':1, 'Medium':2, 'High':3 } %} + {{ mapping.get(trigger.to_state.state, 0) }} + current: "{{ states('input_number.master_bedroom_fan_speed_numeric') | int(0) }}" + condition: + - condition: template + value_template: "{{ desired != current }}" action: - service: mqtt.publish - data: - topic: cmnd/tasmo-ifan02-3793-bedrm1-1/FanSpeed - payload: '{% set mapping = { "Off":"0", "Low":"1", - "Medium":"2", "High":"3" } %} {% set dta = trigger.to_state.state %} - {{ mapping[dta] }} - ' + - service: mqtt.publish + data: + topic: cmnd/tasmo-ifan02-3793-bedrm1-1/FanSpeed + payload: "{{ desired }}" + # Optimistic local update (MQTT will correct if needed) + - service: input_number.set_value + data: + entity_id: input_number.master_bedroom_fan_speed_numeric + value: "{{ desired }}" + + ######################################################################################## + # BEDROOM 2 FAN - Unchanged + ######################################################################################## - id: set_bedroom_2_fan initial_state: true alias: Set Bedroom 2 Fan Speed @@ -53,6 +161,10 @@ automation: "Medium":"2", "High":"3" } %} {% set dta = trigger.to_state.state %} {{ mapping[dta] }} ' + + ######################################################################################## + # BEDROOM 3 FAN - Unchanged + ######################################################################################## - id: set_bedroom_3_fan initial_state: true alias: Set Bedroom 3 Fan Speed