diff --git a/docs/API.md b/docs/API.md index 996859e..c9ecc0f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -930,3 +930,17 @@ Architecture expectation: - Dashboard ESP32-S3 consumes temperature telemetry from the Cargo ESP API. The dashboard should not directly read cargo/fridge DS18B20 sensors for v1. + +### Temperature Sensor Grouping + +Temperature sensors now support a `group` field in addition to the existing `weather` flag. + +Supported group values: + +- `cabin` +- `fridge` +- `outside` +- `battery` +- `other` + +The dashboard overview combines multiple online sensors in the same group into one tile. For example, two fridge sensors may display as `34°/45°` under a single `Fridge` tile. Sensors marked `weather: true` continue to be treated as the outside/weather sensor for dashboard display. diff --git a/docs/dashboard-esp32s3.md b/docs/dashboard-esp32s3.md index 480624d..b0a364e 100644 --- a/docs/dashboard-esp32s3.md +++ b/docs/dashboard-esp32s3.md @@ -248,3 +248,17 @@ Planned next work: - Vehicle data page - CAN/OBD-II integration - BNO085/BNO086 tilt visualization + +### Temperature Sensor Grouping + +Temperature sensors now support a `group` field in addition to the existing `weather` flag. + +Supported group values: + +- `cabin` +- `fridge` +- `outside` +- `battery` +- `other` + +The dashboard overview combines multiple online sensors in the same group into one tile. For example, two fridge sensors may display as `34°/45°` under a single `Fridge` tile. Sensors marked `weather: true` continue to be treated as the outside/weather sensor for dashboard display. diff --git a/docs/project-state.md b/docs/project-state.md index 747e0e8..717e54e 100644 --- a/docs/project-state.md +++ b/docs/project-state.md @@ -315,3 +315,17 @@ Current dashboard features: - WiFi signal indicator - Cargo ESP connectivity indicator - Fast status polling using field-filtered API requests + +### Temperature Sensor Grouping + +Temperature sensors now support a `group` field in addition to the existing `weather` flag. + +Supported group values: + +- `cabin` +- `fridge` +- `outside` +- `battery` +- `other` + +The dashboard overview combines multiple online sensors in the same group into one tile. For example, two fridge sensors may display as `34°/45°` under a single `Fridge` tile. Sensors marked `weather: true` continue to be treated as the outside/weather sensor for dashboard display. diff --git a/firmware/esp32-s3-dashboard/dashboard_config.h b/firmware/esp32-s3-dashboard/dashboard_config.h index fc29ba2..39e26da 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.36-temp-tiles"; +static const char *DASHBOARD_VERSION = "0.1.37-temp-groups"; // 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 b38887a..bb62981 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -19,10 +19,8 @@ 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 *inside_temp_value_label = nullptr; -static lv_obj_t *inside_temp_name_label = nullptr; -static lv_obj_t *outside_temp_value_label = nullptr; -static lv_obj_t *outside_temp_name_label = nullptr; +static lv_obj_t *temp_tile_value_labels[4] = {nullptr}; +static lv_obj_t *temp_tile_name_labels[4] = {nullptr}; 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}; @@ -60,10 +58,8 @@ static bool pending_relay_previous_state = false; static String last_battery_current_text; static String last_battery_estimate_text; static String last_battery_soc_text; -static String last_inside_temp_value_text; -static String last_inside_temp_name_text; -static String last_outside_temp_value_text; -static String last_outside_temp_name_text; +static String last_temp_tile_value_text[4]; +static String last_temp_tile_name_text[4]; static String last_system_status_text; static int last_wifi_bar_count = -1; static bool last_cargo_api_ok = false; @@ -74,18 +70,61 @@ static String on_off(bool value) } -static String compact_temp_name(const char *raw_name) + +static String normalize_temp_group(String group, const String &name, bool weather) { - String name = raw_name && raw_name[0] ? String(raw_name) : String("Inside"); + group.toLowerCase(); + group.trim(); - name.replace(" Area", ""); - name.replace(" Zone", ""); - - if (name.length() > 14) { - name = name.substring(0, 14); + if (weather || group == "weather" || group == "outside") { + return "outside"; } - return name; + if (group == "fridge" || group == "freezer" || group == "cooler") { + return "fridge"; + } + + if (group == "battery" || group == "bms") { + return "battery"; + } + + if (group == "cabin" || group == "inside" || group == "interior") { + return "cabin"; + } + + String lower_name = name; + lower_name.toLowerCase(); + + if (lower_name.indexOf("fridge") >= 0 || lower_name.indexOf("freezer") >= 0 || lower_name.indexOf("cooler") >= 0) { + return "fridge"; + } + + if (lower_name.indexOf("outside") >= 0 || lower_name.indexOf("ambient") >= 0) { + return "outside"; + } + + if (lower_name.indexOf("battery") >= 0 || lower_name.indexOf("bms") >= 0) { + return "battery"; + } + + return group.length() ? group : "cabin"; +} + +static int temp_group_index(const String &group) +{ + if (group == "cabin") return 0; + if (group == "fridge") return 1; + if (group == "outside") return 2; + return 3; +} + +static String temp_group_label(int index, const String &fallback) +{ + if (index == 0) return fallback.length() ? fallback : "Cabin"; + if (index == 1) return "Fridge"; + if (index == 2) return "Outside"; + if (index == 3) return "Other"; + return "Temp"; } static String format_hours(float hours) @@ -611,10 +650,9 @@ static void parse_status_json(const String &body) } if (doc["temps"].is()) { - String inside_name = "Inside"; - String inside_value = "--"; - String outside_name = "Outside"; - String outside_value = "--"; + String tile_names[4] = {"Cabin", "Fridge", "Outside", "Other"}; + String tile_values[4] = {"--", "--", "--", "--"}; + bool tile_has_value[4] = {false, false, false, false}; for (JsonObject temp : doc["temps"].as()) { bool enabled = temp["enabled"] | false; @@ -625,23 +663,38 @@ static void parse_status_json(const String &body) continue; } - String value = String((float)(temp["temperature_f"] | 0.0), 1); + const char *raw_name = temp["name"] | ""; + const char *raw_group = temp["group"] | ""; + + String name = raw_name && raw_name[0] ? String(raw_name) : String("Cabin"); + String group = normalize_temp_group(String(raw_group), name, weather); + int index = temp_group_index(group); + + int rounded = (int)round((float)(temp["temperature_f"] | 0.0)); + String value = String(rounded); value += "°"; - if (weather) { - outside_name = "Outside"; - outside_value = value; + if (tile_has_value[index]) { + tile_values[index] += "/"; + tile_values[index] += value; } else { - const char *name = temp["name"] | "Inside"; - inside_name = compact_temp_name(name); - inside_value = value; + tile_values[index] = value; + tile_has_value[index] = true; + + if (index == 0) { + name.replace(" Area", ""); + name.replace(" Zone", ""); + tile_names[index] = name.length() ? name : "Cabin"; + } else { + tile_names[index] = temp_group_label(index, name); + } } } - set_label_text_if_changed(inside_temp_value_label, last_inside_temp_value_text, inside_value); - set_label_text_if_changed(inside_temp_name_label, last_inside_temp_name_text, inside_name); - set_label_text_if_changed(outside_temp_value_label, last_outside_temp_value_text, outside_value); - set_label_text_if_changed(outside_temp_name_label, last_outside_temp_name_text, outside_name); + for (int i = 0; i < 4; i++) { + set_label_text_if_changed(temp_tile_value_labels[i], last_temp_tile_value_text[i], tile_values[i]); + set_label_text_if_changed(temp_tile_name_labels[i], last_temp_tile_name_text[i], tile_names[i]); + } } if (doc["relays"].is()) { @@ -864,59 +917,39 @@ static void create_overland_overview_screen() lv_obj_align(battery_current_label, LV_ALIGN_TOP_MID, 0, 161); lv_obj_add_flag(battery_current_label, LV_OBJ_FLAG_HIDDEN); - lv_obj_t *inside_tile = lv_obj_create(temp_card); - lv_obj_set_size(inside_tile, 236, 132); - lv_obj_align(inside_tile, LV_ALIGN_TOP_MID, 0, 22); - lv_obj_set_style_radius(inside_tile, 18, 0); - lv_obj_set_style_bg_color(inside_tile, lv_color_hex(0x17202A), 0); - lv_obj_set_style_border_color(inside_tile, lv_color_hex(0x2F3A46), 0); - lv_obj_set_style_border_width(inside_tile, 2, 0); - lv_obj_set_style_pad_all(inside_tile, 0, 0); - lv_obj_clear_flag(inside_tile, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_scrollbar_mode(inside_tile, LV_SCROLLBAR_MODE_OFF); + const int temp_tile_w = 112; + const int temp_tile_h = 130; + const int temp_tile_x[4] = {8, 140, 8, 140}; + const int temp_tile_y[4] = {18, 18, 160, 160}; - inside_temp_value_label = lv_label_create(inside_tile); - lv_label_set_text(inside_temp_value_label, "--"); - lv_obj_set_style_text_color(inside_temp_value_label, lv_color_hex(0xFFFFFF), 0); - lv_obj_set_style_text_font(inside_temp_value_label, &lv_font_montserrat_48, 0); - lv_obj_set_width(inside_temp_value_label, 220); - lv_obj_set_style_text_align(inside_temp_value_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(inside_temp_value_label, LV_ALIGN_TOP_MID, 0, 18); + for (int i = 0; i < 4; i++) { + lv_obj_t *tile = lv_obj_create(temp_card); + lv_obj_set_size(tile, temp_tile_w, temp_tile_h); + lv_obj_align(tile, LV_ALIGN_TOP_LEFT, temp_tile_x[i], temp_tile_y[i]); + lv_obj_set_style_radius(tile, 16, 0); + lv_obj_set_style_bg_color(tile, lv_color_hex(0x17202A), 0); + lv_obj_set_style_border_color(tile, lv_color_hex(0x2F3A46), 0); + lv_obj_set_style_border_width(tile, 2, 0); + lv_obj_set_style_pad_all(tile, 0, 0); + lv_obj_clear_flag(tile, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scrollbar_mode(tile, LV_SCROLLBAR_MODE_OFF); - inside_temp_name_label = lv_label_create(inside_tile); - lv_label_set_text(inside_temp_name_label, "Inside"); - lv_obj_set_style_text_color(inside_temp_name_label, lv_color_hex(0xB8C0C8), 0); - lv_obj_set_style_text_font(inside_temp_name_label, &lv_font_montserrat_26, 0); - lv_obj_set_width(inside_temp_name_label, 220); - lv_obj_set_style_text_align(inside_temp_name_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(inside_temp_name_label, LV_ALIGN_TOP_MID, 0, 84); + temp_tile_value_labels[i] = lv_label_create(tile); + lv_label_set_text(temp_tile_value_labels[i], "--"); + lv_obj_set_style_text_color(temp_tile_value_labels[i], lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(temp_tile_value_labels[i], &lv_font_montserrat_30, 0); + lv_obj_set_width(temp_tile_value_labels[i], temp_tile_w - 8); + lv_obj_set_style_text_align(temp_tile_value_labels[i], LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(temp_tile_value_labels[i], LV_ALIGN_TOP_MID, 0, 26); - lv_obj_t *outside_tile = lv_obj_create(temp_card); - lv_obj_set_size(outside_tile, 236, 132); - lv_obj_align(outside_tile, LV_ALIGN_BOTTOM_MID, 0, -22); - lv_obj_set_style_radius(outside_tile, 18, 0); - lv_obj_set_style_bg_color(outside_tile, lv_color_hex(0x17202A), 0); - lv_obj_set_style_border_color(outside_tile, lv_color_hex(0x2F3A46), 0); - lv_obj_set_style_border_width(outside_tile, 2, 0); - lv_obj_set_style_pad_all(outside_tile, 0, 0); - lv_obj_clear_flag(outside_tile, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_scrollbar_mode(outside_tile, LV_SCROLLBAR_MODE_OFF); - - outside_temp_value_label = lv_label_create(outside_tile); - lv_label_set_text(outside_temp_value_label, "--"); - lv_obj_set_style_text_color(outside_temp_value_label, lv_color_hex(0xFFFFFF), 0); - lv_obj_set_style_text_font(outside_temp_value_label, &lv_font_montserrat_48, 0); - lv_obj_set_width(outside_temp_value_label, 220); - lv_obj_set_style_text_align(outside_temp_value_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(outside_temp_value_label, LV_ALIGN_TOP_MID, 0, 18); - - outside_temp_name_label = lv_label_create(outside_tile); - lv_label_set_text(outside_temp_name_label, "Outside"); - lv_obj_set_style_text_color(outside_temp_name_label, lv_color_hex(0xB8C0C8), 0); - lv_obj_set_style_text_font(outside_temp_name_label, &lv_font_montserrat_26, 0); - lv_obj_set_width(outside_temp_name_label, 220); - lv_obj_set_style_text_align(outside_temp_name_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(outside_temp_name_label, LV_ALIGN_TOP_MID, 0, 84); + temp_tile_name_labels[i] = lv_label_create(tile); + lv_label_set_text(temp_tile_name_labels[i], i == 0 ? "Cabin" : i == 1 ? "Fridge" : i == 2 ? "Outside" : "Other"); + lv_obj_set_style_text_color(temp_tile_name_labels[i], lv_color_hex(0xB8C0C8), 0); + lv_obj_set_style_text_font(temp_tile_name_labels[i], &lv_font_montserrat_20, 0); + lv_obj_set_width(temp_tile_name_labels[i], temp_tile_w - 8); + lv_obj_set_style_text_align(temp_tile_name_labels[i], LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(temp_tile_name_labels[i], LV_ALIGN_TOP_MID, 0, 78); + } lv_obj_t *vehicle_label = lv_label_create(vehicle_card); lv_label_set_text(vehicle_label, "Vehicle\n\nOffline\n\nFuture:\nCoolant\nTrans Temp\n4WD\nTilt/Roll"); diff --git a/firmware/esp32/overland-controller/app_config.cpp b/firmware/esp32/overland-controller/app_config.cpp index 5d80a8a..264bd21 100644 --- a/firmware/esp32/overland-controller/app_config.cpp +++ b/firmware/esp32/overland-controller/app_config.cpp @@ -38,7 +38,8 @@ void loadDefaultConfig() { "Temperature " + String(i + 1), "", i < appConfig.tempSensorCount, - false + false, + "cabin" }; } @@ -101,6 +102,10 @@ void loadConfig() { (prefix + "weather").c_str(), appConfig.tempSensors[i].weather ); + appConfig.tempSensors[i].group = preferences.getString( + (prefix + "group").c_str(), + appConfig.tempSensors[i].group + ); } appConfig.bms.enabled = preferences.getBool("bms_enabled", appConfig.bms.enabled); @@ -136,6 +141,7 @@ void saveConfig() { preferences.putString((prefix + "addr").c_str(), appConfig.tempSensors[i].address); preferences.putBool((prefix + "enabled").c_str(), appConfig.tempSensors[i].enabled); preferences.putBool((prefix + "weather").c_str(), appConfig.tempSensors[i].weather); + preferences.putString((prefix + "group").c_str(), appConfig.tempSensors[i].group); } preferences.putBool("bms_enabled", appConfig.bms.enabled); @@ -187,7 +193,9 @@ void printConfig() { Serial.print(" / enabled "); Serial.print(appConfig.tempSensors[i].enabled ? "true" : "false"); Serial.print(" / address "); - Serial.println(appConfig.tempSensors[i].address); + Serial.print(appConfig.tempSensors[i].address); + Serial.print(" / group "); + Serial.println(appConfig.tempSensors[i].group); } Serial.println(" BMS:"); diff --git a/firmware/esp32/overland-controller/app_config.h b/firmware/esp32/overland-controller/app_config.h index 3cfe7e0..d838e18 100644 --- a/firmware/esp32/overland-controller/app_config.h +++ b/firmware/esp32/overland-controller/app_config.h @@ -22,6 +22,7 @@ struct TempSensorConfig { String address; bool enabled; bool weather; + String group; }; struct BmsConfig { diff --git a/firmware/esp32/overland-controller/overland-controller.ino b/firmware/esp32/overland-controller/overland-controller.ino index d6e7410..ca57411 100644 --- a/firmware/esp32/overland-controller/overland-controller.ino +++ b/firmware/esp32/overland-controller/overland-controller.ino @@ -1183,6 +1183,15 @@ function renderConfigControls(data){
+ + +