From 9bcc9b9b4dbc124e260c6f599ebbdce2984eb16b Mon Sep 17 00:00:00 2001 From: ESPHome Device Builder Date: Sat, 18 Jul 2026 21:14:53 +1200 Subject: [PATCH] Edit esp-bedside-panel.yaml --- esphome/esp-bedside-panel.yaml | 1624 +++++++++++++++++++++++++++++++- 1 file changed, 1621 insertions(+), 3 deletions(-) diff --git a/esphome/esp-bedside-panel.yaml b/esphome/esp-bedside-panel.yaml index 8b1fdb5..da4c776 100644 --- a/esphome/esp-bedside-panel.yaml +++ b/esphome/esp-bedside-panel.yaml @@ -6,6 +6,7 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedside-panel.yaml #:########################################################################################:# # VERSIONS: +# V1.22 2026-07-18 Added 15-slot Jobs page with status colours and hold completion # V1.21 2026-07-18 Replaced bottom navigation text with icons and live time # V1.20 2026-07-18 Swapped Bedtime/Jobs, defaulted Doorbell and refined Climate icons # V1.18 2026-07-18 Added Lounge Fire, six Security cameras, Jobs page and weather/tide header @@ -122,12 +123,12 @@ substitutions: # Device Naming device_name: "esp-bedside-panel" friendly_name: "ESP Bedside Panel" - description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside control panel. Icon-only bottom navigation with live 24-hour clock buttons. (Layout V1.1)" + description_comment: "Guition JC1060P470C_I_W_Y ESP32-P4 bedside control panel. Fifteen-slot Jobs page with live status and hold-to-complete controls. (Layout V1.1)" device_area: "Bedroom" # Project Naming project_name: "Guition.JC1060P470C_I_W_Y" - project_version: "v1.21" + project_version: "v1.22" # Passwords & Secrets api_key: !secret esp-api_key @@ -225,6 +226,23 @@ substitutions: downstairs_guests_occupied_entity: "input_boolean.downstairs_flat_occupied" external_garage_light_entity: "switch.esp_garageextcornerlight_garage_external_light" + # Home Assistant Entities - Jobs Tracker + # Slots 1-5 are currently active. The page reserves 15 positions in total. + job_1_state_entity: "sensor.jobs_tracker_dog_feeding_morning" + job_1_complete_button_entity: "button.jobs_tracker_complete_dog_feeding_morning" + + job_2_state_entity: "sensor.jobs_tracker_dog_feeding_evening" + job_2_complete_button_entity: "button.jobs_tracker_complete_dog_feeding_evening" + + job_3_state_entity: "sensor.jobs_tracker_dog_depooping" + job_3_complete_button_entity: "button.jobs_tracker_complete_dog_depooping" + + job_4_state_entity: "sensor.jobs_tracker_dishwasher_unpacked" + job_4_complete_button_entity: "button.jobs_tracker_complete_dishwasher_unpacked" + + job_5_state_entity: "sensor.jobs_tracker_rubbish_removed" + job_5_complete_button_entity: "button.jobs_tracker_complete_rubbish_removed" + #:########################################################################################:# # PACKAGES: # Included Common Packages @@ -293,6 +311,7 @@ esphome: - script.execute: refresh_button_states - script.execute: refresh_sleep_timer - script.execute: refresh_navigation_time + - script.execute: refresh_jobs_page #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: @@ -820,6 +839,144 @@ sensor: - script.execute: refresh_vehicle_controls + #:######################################################################################:# + # JOBS TRACKER NUMERIC ATTRIBUTES + #:######################################################################################:# + - platform: homeassistant + id: job_1_hours_since_completed + entity_id: "${job_1_state_entity}" + attribute: hours_since_completed + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_1_hours_since_required + entity_id: "${job_1_state_entity}" + attribute: hours_since_required + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_1_display_due_ts + entity_id: "${job_1_state_entity}" + attribute: display_due_ts + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_2_hours_since_completed + entity_id: "${job_2_state_entity}" + attribute: hours_since_completed + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_2_hours_since_required + entity_id: "${job_2_state_entity}" + attribute: hours_since_required + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_2_display_due_ts + entity_id: "${job_2_state_entity}" + attribute: display_due_ts + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_3_hours_since_completed + entity_id: "${job_3_state_entity}" + attribute: hours_since_completed + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_3_hours_since_required + entity_id: "${job_3_state_entity}" + attribute: hours_since_required + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_3_display_due_ts + entity_id: "${job_3_state_entity}" + attribute: display_due_ts + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_4_hours_since_completed + entity_id: "${job_4_state_entity}" + attribute: hours_since_completed + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_4_hours_since_required + entity_id: "${job_4_state_entity}" + attribute: hours_since_required + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_4_display_due_ts + entity_id: "${job_4_state_entity}" + attribute: display_due_ts + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_5_hours_since_completed + entity_id: "${job_5_state_entity}" + attribute: hours_since_completed + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_5_hours_since_required + entity_id: "${job_5_state_entity}" + attribute: hours_since_required + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_5_display_due_ts + entity_id: "${job_5_state_entity}" + attribute: display_due_ts + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + #:########################################################################################:# # HOME ASSISTANT BINARY SENSORS: # Imported states used to colour LVGL buttons @@ -1129,6 +1286,1048 @@ script: now.strftime(buffer, sizeof(buffer), "%H:%M"); return std::string(buffer); + #:######################################################################################:# + # JOBS PAGE STATE REFRESH + #:######################################################################################:# + - id: refresh_jobs_page + mode: restart + then: + - if: + condition: + lambda: |- + return id(lvgl_ready); + + then: + - lvgl.widget.update: + id: job_1_button + bg_color: !lambda |- + const auto state = id(job_1_state).state; + + if (state == "Complete") { + return lv_color_hex(0x2E7D32); + } + + if (state == "Not Due") { + return lv_color_hex(0x7E8995); + } + + if (state == "Due") { + return lv_color_hex(0xB08A18); + } + + if (state == "Overdue") { + return lv_color_hex(0xC66A1B); + } + + if (state == "Missed") { + return lv_color_hex(0xB53636); + } + + return lv_color_hex(0x28313D); + + + - lvgl.label.update: + id: job_1_status_label + text: !lambda |- + const auto state = id(job_1_state).state; + + if (state == "Complete") { + const float raw_hours = + id(job_1_hours_since_completed).state; + + if (isnan(raw_hours) || raw_hours < 0.0f) { + return std::string("Complete"); + } + + const int hours = (int) lroundf(raw_hours); + + if (hours <= 0) { + return std::string("Complete - Just now"); + } + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Complete - 1 hr ago" + : "Complete - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Complete - 1 day ago" + : "Complete - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Not Due") { + const float due_ts = id(job_1_display_due_ts).state; + const auto now = id(ha_time).now(); + + if ( + isnan(due_ts) || + due_ts <= 0.0f || + !now.is_valid() + ) { + return std::string("Not Due"); + } + + const int64_t seconds_remaining = + (int64_t) due_ts - (int64_t) now.timestamp; + + if (seconds_remaining <= 0) { + return std::string("Not Due - Soon"); + } + + const int hours = + (int) ((seconds_remaining + 3599) / 3600); + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Not Due - In 1 hr" + : "Not Due - In %d hrs", + hours + ); + } else { + const int days = (hours + 23) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Not Due - In 1 day" + : "Not Due - In %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Due") { + const float raw_hours = + id(job_1_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Due - Just now"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Due - 1 hr ago" + : "Due - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Due - 1 day ago" + : "Due - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Overdue") { + const float raw_hours = + id(job_1_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Overdue"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Overdue - 1 hr" + : "Overdue - %d hrs", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Overdue - 1 day" + : "Overdue - %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Missed") { + return std::string("Missed"); + } + + if ( + state.empty() || + state == "unknown" || + state == "unavailable" + ) { + return std::string("Unknown"); + } + + return state; + + + - lvgl.widget.update: + id: job_2_button + bg_color: !lambda |- + const auto state = id(job_2_state).state; + + if (state == "Complete") { + return lv_color_hex(0x2E7D32); + } + + if (state == "Not Due") { + return lv_color_hex(0x7E8995); + } + + if (state == "Due") { + return lv_color_hex(0xB08A18); + } + + if (state == "Overdue") { + return lv_color_hex(0xC66A1B); + } + + if (state == "Missed") { + return lv_color_hex(0xB53636); + } + + return lv_color_hex(0x28313D); + + + - lvgl.label.update: + id: job_2_status_label + text: !lambda |- + const auto state = id(job_2_state).state; + + if (state == "Complete") { + const float raw_hours = + id(job_2_hours_since_completed).state; + + if (isnan(raw_hours) || raw_hours < 0.0f) { + return std::string("Complete"); + } + + const int hours = (int) lroundf(raw_hours); + + if (hours <= 0) { + return std::string("Complete - Just now"); + } + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Complete - 1 hr ago" + : "Complete - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Complete - 1 day ago" + : "Complete - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Not Due") { + const float due_ts = id(job_2_display_due_ts).state; + const auto now = id(ha_time).now(); + + if ( + isnan(due_ts) || + due_ts <= 0.0f || + !now.is_valid() + ) { + return std::string("Not Due"); + } + + const int64_t seconds_remaining = + (int64_t) due_ts - (int64_t) now.timestamp; + + if (seconds_remaining <= 0) { + return std::string("Not Due - Soon"); + } + + const int hours = + (int) ((seconds_remaining + 3599) / 3600); + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Not Due - In 1 hr" + : "Not Due - In %d hrs", + hours + ); + } else { + const int days = (hours + 23) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Not Due - In 1 day" + : "Not Due - In %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Due") { + const float raw_hours = + id(job_2_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Due - Just now"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Due - 1 hr ago" + : "Due - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Due - 1 day ago" + : "Due - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Overdue") { + const float raw_hours = + id(job_2_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Overdue"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Overdue - 1 hr" + : "Overdue - %d hrs", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Overdue - 1 day" + : "Overdue - %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Missed") { + return std::string("Missed"); + } + + if ( + state.empty() || + state == "unknown" || + state == "unavailable" + ) { + return std::string("Unknown"); + } + + return state; + + + - lvgl.widget.update: + id: job_3_button + bg_color: !lambda |- + const auto state = id(job_3_state).state; + + if (state == "Complete") { + return lv_color_hex(0x2E7D32); + } + + if (state == "Not Due") { + return lv_color_hex(0x7E8995); + } + + if (state == "Due") { + return lv_color_hex(0xB08A18); + } + + if (state == "Overdue") { + return lv_color_hex(0xC66A1B); + } + + if (state == "Missed") { + return lv_color_hex(0xB53636); + } + + return lv_color_hex(0x28313D); + + + - lvgl.label.update: + id: job_3_status_label + text: !lambda |- + const auto state = id(job_3_state).state; + + if (state == "Complete") { + const float raw_hours = + id(job_3_hours_since_completed).state; + + if (isnan(raw_hours) || raw_hours < 0.0f) { + return std::string("Complete"); + } + + const int hours = (int) lroundf(raw_hours); + + if (hours <= 0) { + return std::string("Complete - Just now"); + } + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Complete - 1 hr ago" + : "Complete - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Complete - 1 day ago" + : "Complete - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Not Due") { + const float due_ts = id(job_3_display_due_ts).state; + const auto now = id(ha_time).now(); + + if ( + isnan(due_ts) || + due_ts <= 0.0f || + !now.is_valid() + ) { + return std::string("Not Due"); + } + + const int64_t seconds_remaining = + (int64_t) due_ts - (int64_t) now.timestamp; + + if (seconds_remaining <= 0) { + return std::string("Not Due - Soon"); + } + + const int hours = + (int) ((seconds_remaining + 3599) / 3600); + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Not Due - In 1 hr" + : "Not Due - In %d hrs", + hours + ); + } else { + const int days = (hours + 23) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Not Due - In 1 day" + : "Not Due - In %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Due") { + const float raw_hours = + id(job_3_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Due - Just now"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Due - 1 hr ago" + : "Due - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Due - 1 day ago" + : "Due - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Overdue") { + const float raw_hours = + id(job_3_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Overdue"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Overdue - 1 hr" + : "Overdue - %d hrs", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Overdue - 1 day" + : "Overdue - %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Missed") { + return std::string("Missed"); + } + + if ( + state.empty() || + state == "unknown" || + state == "unavailable" + ) { + return std::string("Unknown"); + } + + return state; + + + - lvgl.widget.update: + id: job_4_button + bg_color: !lambda |- + const auto state = id(job_4_state).state; + + if (state == "Complete") { + return lv_color_hex(0x2E7D32); + } + + if (state == "Not Due") { + return lv_color_hex(0x7E8995); + } + + if (state == "Due") { + return lv_color_hex(0xB08A18); + } + + if (state == "Overdue") { + return lv_color_hex(0xC66A1B); + } + + if (state == "Missed") { + return lv_color_hex(0xB53636); + } + + return lv_color_hex(0x28313D); + + + - lvgl.label.update: + id: job_4_status_label + text: !lambda |- + const auto state = id(job_4_state).state; + + if (state == "Complete") { + const float raw_hours = + id(job_4_hours_since_completed).state; + + if (isnan(raw_hours) || raw_hours < 0.0f) { + return std::string("Complete"); + } + + const int hours = (int) lroundf(raw_hours); + + if (hours <= 0) { + return std::string("Complete - Just now"); + } + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Complete - 1 hr ago" + : "Complete - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Complete - 1 day ago" + : "Complete - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Not Due") { + const float due_ts = id(job_4_display_due_ts).state; + const auto now = id(ha_time).now(); + + if ( + isnan(due_ts) || + due_ts <= 0.0f || + !now.is_valid() + ) { + return std::string("Not Due"); + } + + const int64_t seconds_remaining = + (int64_t) due_ts - (int64_t) now.timestamp; + + if (seconds_remaining <= 0) { + return std::string("Not Due - Soon"); + } + + const int hours = + (int) ((seconds_remaining + 3599) / 3600); + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Not Due - In 1 hr" + : "Not Due - In %d hrs", + hours + ); + } else { + const int days = (hours + 23) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Not Due - In 1 day" + : "Not Due - In %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Due") { + const float raw_hours = + id(job_4_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Due - Just now"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Due - 1 hr ago" + : "Due - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Due - 1 day ago" + : "Due - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Overdue") { + const float raw_hours = + id(job_4_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Overdue"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Overdue - 1 hr" + : "Overdue - %d hrs", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Overdue - 1 day" + : "Overdue - %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Missed") { + return std::string("Missed"); + } + + if ( + state.empty() || + state == "unknown" || + state == "unavailable" + ) { + return std::string("Unknown"); + } + + return state; + + + - lvgl.widget.update: + id: job_5_button + bg_color: !lambda |- + const auto state = id(job_5_state).state; + + if (state == "Complete") { + return lv_color_hex(0x2E7D32); + } + + if (state == "Not Due") { + return lv_color_hex(0x7E8995); + } + + if (state == "Due") { + return lv_color_hex(0xB08A18); + } + + if (state == "Overdue") { + return lv_color_hex(0xC66A1B); + } + + if (state == "Missed") { + return lv_color_hex(0xB53636); + } + + return lv_color_hex(0x28313D); + + + - lvgl.label.update: + id: job_5_status_label + text: !lambda |- + const auto state = id(job_5_state).state; + + if (state == "Complete") { + const float raw_hours = + id(job_5_hours_since_completed).state; + + if (isnan(raw_hours) || raw_hours < 0.0f) { + return std::string("Complete"); + } + + const int hours = (int) lroundf(raw_hours); + + if (hours <= 0) { + return std::string("Complete - Just now"); + } + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Complete - 1 hr ago" + : "Complete - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Complete - 1 day ago" + : "Complete - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Not Due") { + const float due_ts = id(job_5_display_due_ts).state; + const auto now = id(ha_time).now(); + + if ( + isnan(due_ts) || + due_ts <= 0.0f || + !now.is_valid() + ) { + return std::string("Not Due"); + } + + const int64_t seconds_remaining = + (int64_t) due_ts - (int64_t) now.timestamp; + + if (seconds_remaining <= 0) { + return std::string("Not Due - Soon"); + } + + const int hours = + (int) ((seconds_remaining + 3599) / 3600); + + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Not Due - In 1 hr" + : "Not Due - In %d hrs", + hours + ); + } else { + const int days = (hours + 23) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Not Due - In 1 day" + : "Not Due - In %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Due") { + const float raw_hours = + id(job_5_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Due - Just now"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Due - 1 hr ago" + : "Due - %d hrs ago", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Due - 1 day ago" + : "Due - %d days ago", + days + ); + } + + return std::string(buffer); + } + + if (state == "Overdue") { + const float raw_hours = + id(job_5_hours_since_required).state; + + if (isnan(raw_hours) || raw_hours <= 0.0f) { + return std::string("Overdue"); + } + + const int hours = (int) lroundf(raw_hours); + char buffer[48]; + + if (hours < 24) { + snprintf( + buffer, + sizeof(buffer), + hours == 1 + ? "Overdue - 1 hr" + : "Overdue - %d hrs", + hours + ); + } else { + const int days = (hours + 12) / 24; + snprintf( + buffer, + sizeof(buffer), + days == 1 + ? "Overdue - 1 day" + : "Overdue - %d days", + days + ); + } + + return std::string(buffer); + } + + if (state == "Missed") { + return std::string("Missed"); + } + + if ( + state.empty() || + state == "unknown" || + state == "unavailable" + ) { + return std::string("Unknown"); + } + + return state; + + # Update Bedroom and Clock status values after LVGL has finished constructing. - id: refresh_header_status mode: restart @@ -2249,6 +3448,7 @@ interval: then: - script.execute: refresh_navigation_time + - script.execute: refresh_jobs_page - lvgl.label.update: id: clock_time_label @@ -2400,6 +3600,49 @@ text_sensor: then: - script.execute: refresh_climate_controls + #:######################################################################################:# + # JOBS TRACKER STATES + #:######################################################################################:# + - platform: homeassistant + id: job_1_state + entity_id: "${job_1_state_entity}" + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_2_state + entity_id: "${job_2_state_entity}" + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_3_state + entity_id: "${job_3_state_entity}" + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_4_state + entity_id: "${job_4_state_entity}" + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + + - platform: homeassistant + id: job_5_state + entity_id: "${job_5_state_entity}" + internal: true + on_value: + then: + - script.execute: refresh_jobs_page + #:######################################################################################:# # ATTO 3 TEXT STATES #:######################################################################################:# @@ -3344,6 +4587,8 @@ lvgl: animation: MOVE_LEFT time: 220ms + - script.execute: refresh_jobs_page + - button: id: climate_navigation_button x: 414 @@ -3487,7 +4732,380 @@ lvgl: bg_opa: COVER widgets: - # Reserved for the future Jobs interface. + #:################################################################################:# + # ACTIVE JOBS + #:################################################################################:# + - button: + id: job_1_button + x: 18 + y: 18 + width: 320 + height: 76 + bg_color: 0x28313D + bg_opa: COVER + border_width: 2 + border_color: 0x526276 + radius: 14 + pressed: + bg_color: 0x42627A + widgets: + - label: + x: 10 + y: 8 + width: 300 + text: "Dog Feeding Morning" + text_font: font_small + text_align: CENTER + text_color: 0xFFFFFF + + - label: + id: job_1_status_label + x: 10 + y: 42 + width: 300 + text: "Unknown" + text_font: font_header + text_align: CENTER + text_color: 0xFFFFFF + + on_long_press: + then: + - lvgl.label.update: + id: job_1_status_label + text: "Completing..." + + - homeassistant.action: + action: button.press + data: + entity_id: "${job_1_complete_button_entity}" + + - delay: 2s + - script.execute: refresh_jobs_page + + - button: + id: job_2_button + x: 352 + y: 18 + width: 320 + height: 76 + bg_color: 0x28313D + bg_opa: COVER + border_width: 2 + border_color: 0x526276 + radius: 14 + pressed: + bg_color: 0x42627A + widgets: + - label: + x: 10 + y: 8 + width: 300 + text: "Dog Feeding Evening" + text_font: font_small + text_align: CENTER + text_color: 0xFFFFFF + + - label: + id: job_2_status_label + x: 10 + y: 42 + width: 300 + text: "Unknown" + text_font: font_header + text_align: CENTER + text_color: 0xFFFFFF + + on_long_press: + then: + - lvgl.label.update: + id: job_2_status_label + text: "Completing..." + + - homeassistant.action: + action: button.press + data: + entity_id: "${job_2_complete_button_entity}" + + - delay: 2s + - script.execute: refresh_jobs_page + + - button: + id: job_3_button + x: 686 + y: 18 + width: 320 + height: 76 + bg_color: 0x28313D + bg_opa: COVER + border_width: 2 + border_color: 0x526276 + radius: 14 + pressed: + bg_color: 0x42627A + widgets: + - label: + x: 10 + y: 8 + width: 300 + text: "Dog De-pooping" + text_font: font_small + text_align: CENTER + text_color: 0xFFFFFF + + - label: + id: job_3_status_label + x: 10 + y: 42 + width: 300 + text: "Unknown" + text_font: font_header + text_align: CENTER + text_color: 0xFFFFFF + + on_long_press: + then: + - lvgl.label.update: + id: job_3_status_label + text: "Completing..." + + - homeassistant.action: + action: button.press + data: + entity_id: "${job_3_complete_button_entity}" + + - delay: 2s + - script.execute: refresh_jobs_page + + - button: + id: job_4_button + x: 18 + y: 103 + width: 320 + height: 76 + bg_color: 0x28313D + bg_opa: COVER + border_width: 2 + border_color: 0x526276 + radius: 14 + pressed: + bg_color: 0x42627A + widgets: + - label: + x: 10 + y: 8 + width: 300 + text: "Dishwasher Unpacked" + text_font: font_small + text_align: CENTER + text_color: 0xFFFFFF + + - label: + id: job_4_status_label + x: 10 + y: 42 + width: 300 + text: "Unknown" + text_font: font_header + text_align: CENTER + text_color: 0xFFFFFF + + on_long_press: + then: + - lvgl.label.update: + id: job_4_status_label + text: "Completing..." + + - homeassistant.action: + action: button.press + data: + entity_id: "${job_4_complete_button_entity}" + + - delay: 2s + - script.execute: refresh_jobs_page + + - button: + id: job_5_button + x: 352 + y: 103 + width: 320 + height: 76 + bg_color: 0x28313D + bg_opa: COVER + border_width: 2 + border_color: 0x526276 + radius: 14 + pressed: + bg_color: 0x42627A + widgets: + - label: + x: 10 + y: 8 + width: 300 + text: "Rubbish Removed" + text_font: font_small + text_align: CENTER + text_color: 0xFFFFFF + + - label: + id: job_5_status_label + x: 10 + y: 42 + width: 300 + text: "Unknown" + text_font: font_header + text_align: CENTER + text_color: 0xFFFFFF + + on_long_press: + then: + - lvgl.label.update: + id: job_5_status_label + text: "Completing..." + + - homeassistant.action: + action: button.press + data: + entity_id: "${job_5_complete_button_entity}" + + - delay: 2s + - script.execute: refresh_jobs_page + + #:################################################################################:# + # RESERVED JOB SLOTS 6-15 + #:################################################################################:# + - obj: + id: job_slot_6 + x: 686 + y: 103 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_7 + x: 18 + y: 188 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_8 + x: 352 + y: 188 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_9 + x: 686 + y: 188 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_10 + x: 18 + y: 273 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_11 + x: 352 + y: 273 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_12 + x: 686 + y: 273 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_13 + x: 18 + y: 358 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_14 + x: 352 + y: 358 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + - obj: + id: job_slot_15 + x: 686 + y: 358 + width: 320 + height: 76 + bg_color: 0x1B2530 + bg_opa: COVER + border_width: 1 + border_color: 0x334150 + radius: 14 + clickable: false + + #:################################################################################:# + # BOTTOM NAVIGATION + #:################################################################################:# - button: id: jobs_back_button x: 612