diff --git a/docs/API.md b/docs/API.md index 6b57311..e886b84 100644 --- a/docs/API.md +++ b/docs/API.md @@ -963,3 +963,13 @@ The Cargo ESP currently supports 4 temperature sensor slots. Temperature sensor `group` values are free-form text in the WebUI. Entering a new group name effectively creates that group. The dashboard overview shows the first two available non-weather groups by priority. Multiple sensors in the same group are combined into one tile, such as `34°/45°`. Sensors marked `weather: true` are excluded from the temperature card and shown in the top status strip as `OUT ##°`. + +### Temperature High Alerts + +Each temperature sensor supports optional high-temperature alerting: + +- `high_alert_enabled` +- `high_alert_f` +- `high_alert` + +When enabled and the live sensor temperature exceeds the configured threshold, the Cargo ESP includes `high_alert: true` for that sensor in `/api/v1/status`. The ESP32-S3 dashboard colors the affected grouped temperature tile red if any sensor in that displayed group is in alert. diff --git a/docs/dashboard-esp32s3.md b/docs/dashboard-esp32s3.md index de78b1a..b4a7cd3 100644 --- a/docs/dashboard-esp32s3.md +++ b/docs/dashboard-esp32s3.md @@ -281,3 +281,13 @@ The Cargo ESP currently supports 4 temperature sensor slots. Temperature sensor `group` values are free-form text in the WebUI. Entering a new group name effectively creates that group. The dashboard overview shows the first two available non-weather groups by priority. Multiple sensors in the same group are combined into one tile, such as `34°/45°`. Sensors marked `weather: true` are excluded from the temperature card and shown in the top status strip as `OUT ##°`. + +### Temperature High Alerts + +Each temperature sensor supports optional high-temperature alerting: + +- `high_alert_enabled` +- `high_alert_f` +- `high_alert` + +When enabled and the live sensor temperature exceeds the configured threshold, the Cargo ESP includes `high_alert: true` for that sensor in `/api/v1/status`. The ESP32-S3 dashboard colors the affected grouped temperature tile red if any sensor in that displayed group is in alert. diff --git a/docs/project-state.md b/docs/project-state.md index 6aacfb8..78781e7 100644 --- a/docs/project-state.md +++ b/docs/project-state.md @@ -348,3 +348,13 @@ The Cargo ESP currently supports 4 temperature sensor slots. Temperature sensor `group` values are free-form text in the WebUI. Entering a new group name effectively creates that group. The dashboard overview shows the first two available non-weather groups by priority. Multiple sensors in the same group are combined into one tile, such as `34°/45°`. Sensors marked `weather: true` are excluded from the temperature card and shown in the top status strip as `OUT ##°`. + +### Temperature High Alerts + +Each temperature sensor supports optional high-temperature alerting: + +- `high_alert_enabled` +- `high_alert_f` +- `high_alert` + +When enabled and the live sensor temperature exceeds the configured threshold, the Cargo ESP includes `high_alert: true` for that sensor in `/api/v1/status`. The ESP32-S3 dashboard colors the affected grouped temperature tile red if any sensor in that displayed group is in alert. diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index fed8af1..f84270e 100644 --- a/firmware/esp32-s3-dashboard/dashboard_config.h +++ b/firmware/esp32-s3-dashboard/dashboard_config.h @@ -1,6 +1,6 @@ #pragma once -static const char *DASHBOARD_VERSION = "0.1.40-temp-two-groups"; +static const char *DASHBOARD_VERSION = "0.1.41-temp-alerts"; // Update these if your Cargo ESP AP credentials are different. static const char *CARGO_WIFI_SSID = "OverlandController"; diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index 36a3c5a..f5ea4ab 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -19,8 +19,10 @@ static lv_obj_t *battery_charge_pulse_arc = nullptr; static lv_obj_t *battery_current_label = nullptr; static lv_obj_t *battery_estimate_label = nullptr; static lv_obj_t *battery_soc_label = nullptr; +static lv_obj_t *temp_tile_objs[2] = {nullptr}; static lv_obj_t *temp_tile_value_labels[2] = {nullptr}; static lv_obj_t *temp_tile_name_labels[2] = {nullptr}; +static bool last_temp_tile_alert[2] = {false, false}; static lv_obj_t *system_status_label = nullptr; static lv_obj_t *wifi_icon_label = nullptr; static lv_obj_t *wifi_bars_obj[4] = {nullptr}; @@ -597,6 +599,34 @@ static void update_system_status_label() + +static void update_temp_tile_alert_style(int index, bool alert) +{ + if (index < 0 || index >= 2 || temp_tile_objs[index] == nullptr) { + return; + } + + if (last_temp_tile_alert[index] == alert) { + return; + } + + last_temp_tile_alert[index] = alert; + + lvgl_port_lock(-1); + + if (alert) { + lv_obj_set_style_bg_color(temp_tile_objs[index], lv_color_hex(0x7A271A), 0); + lv_obj_set_style_border_color(temp_tile_objs[index], lv_color_hex(0xF97066), 0); + lv_obj_set_style_border_width(temp_tile_objs[index], 3, 0); + } else { + lv_obj_set_style_bg_color(temp_tile_objs[index], lv_color_hex(0x17202A), 0); + lv_obj_set_style_border_color(temp_tile_objs[index], lv_color_hex(0x2F3A46), 0); + lv_obj_set_style_border_width(temp_tile_objs[index], 2, 0); + } + + lvgl_port_unlock(); +} + static void update_overview_widgets() { String current_text = "Idle"; @@ -687,6 +717,7 @@ static void parse_status_json(const String &body) String group; int priority; int rounded_f; + bool high_alert; }; TempDisplayItem items[4]; @@ -702,6 +733,7 @@ static void parse_status_json(const String &body) } float temp_f = (float)(temp["temperature_f"] | 0.0); + bool high_alert = temp["high_alert"] | false; if (weather) { outside_status = "OUT "; @@ -716,7 +748,6 @@ static void parse_status_json(const String &body) String name = raw_name && raw_name[0] ? String(raw_name) : String("cabin"); String group = normalize_temp_group(String(raw_group), name, false); - // Outside/weather is shown in the status strip, not the main temp card. if (group == "outside") { outside_status = "OUT "; outside_status += String((int)round(temp_f)); @@ -728,7 +759,8 @@ static void parse_status_json(const String &body) items[item_count] = { group, temp["priority"] | 99, - (int)round(temp_f) + (int)round(temp_f), + high_alert }; item_count++; } @@ -749,13 +781,15 @@ static void parse_status_json(const String &body) String group_names[2] = {"--", "--"}; String group_values[2] = {"--", "--"}; + bool group_alerts[2] = {false, false}; int group_count = 0; for (int i = 0; i < item_count; i++) { int slot = -1; + String display_group = display_temp_group_name(items[i].group); for (int g = 0; g < group_count; g++) { - if (group_names[g] == display_temp_group_name(items[i].group)) { + if (group_names[g] == display_group) { slot = g; break; } @@ -767,7 +801,7 @@ static void parse_status_json(const String &body) } slot = group_count; - group_names[slot] = display_temp_group_name(items[i].group); + group_names[slot] = display_group; group_values[slot] = ""; group_count++; } @@ -780,6 +814,10 @@ static void parse_status_json(const String &body) } group_values[slot] += value; + + if (items[i].high_alert) { + group_alerts[slot] = true; + } } update_outside_status_temp(outside_status); @@ -787,6 +825,7 @@ static void parse_status_json(const String &body) for (int i = 0; i < 2; i++) { set_label_text_if_changed(temp_tile_value_labels[i], last_temp_tile_value_text[i], group_values[i]); set_label_text_if_changed(temp_tile_name_labels[i], last_temp_tile_name_text[i], group_names[i]); + update_temp_tile_alert_style(i, group_alerts[i]); } } @@ -1016,6 +1055,7 @@ static void create_overland_overview_screen() for (int i = 0; i < 2; i++) { lv_obj_t *tile = lv_obj_create(temp_card); + temp_tile_objs[i] = tile; lv_obj_set_size(tile, temp_tile_w, temp_tile_h); lv_obj_align(tile, LV_ALIGN_TOP_MID, 0, temp_tile_y[i]); lv_obj_set_style_radius(tile, 18, 0); diff --git a/firmware/esp32/overland-controller/app_config.cpp b/firmware/esp32/overland-controller/app_config.cpp index 39431ed..4f36755 100644 --- a/firmware/esp32/overland-controller/app_config.cpp +++ b/firmware/esp32/overland-controller/app_config.cpp @@ -40,7 +40,9 @@ void loadDefaultConfig() { i < appConfig.tempSensorCount, false, "cabin", - i + 1 + i + 1, + false, + 90.0 }; } @@ -111,6 +113,14 @@ void loadConfig() { (prefix + "priority").c_str(), appConfig.tempSensors[i].priority ); + appConfig.tempSensors[i].highAlertEnabled = preferences.getBool( + (prefix + "high_alert_enabled").c_str(), + appConfig.tempSensors[i].highAlertEnabled + ); + appConfig.tempSensors[i].highAlertF = preferences.getFloat( + (prefix + "high_alert_f").c_str(), + appConfig.tempSensors[i].highAlertF + ); } appConfig.bms.enabled = preferences.getBool("bms_enabled", appConfig.bms.enabled); @@ -203,7 +213,11 @@ void printConfig() { Serial.print(" / group "); Serial.print(appConfig.tempSensors[i].group); Serial.print(" / priority "); - Serial.println(appConfig.tempSensors[i].priority); + Serial.print(appConfig.tempSensors[i].priority); + Serial.print(" / high alert "); + Serial.print(appConfig.tempSensors[i].highAlertEnabled ? "on" : "off"); + Serial.print(" @ "); + Serial.println(appConfig.tempSensors[i].highAlertF); } Serial.println(" BMS:"); diff --git a/firmware/esp32/overland-controller/app_config.h b/firmware/esp32/overland-controller/app_config.h index 556fe41..a3bf5ca 100644 --- a/firmware/esp32/overland-controller/app_config.h +++ b/firmware/esp32/overland-controller/app_config.h @@ -24,6 +24,8 @@ struct TempSensorConfig { bool weather; String group; int priority; + bool highAlertEnabled; + float highAlertF; }; struct BmsConfig { diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index 45c23c9..11415b4 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -1196,6 +1196,14 @@ function renderConfigControls(data){ +