From 43f416ab69a5a90938b2c1a2d50759707bbac056 Mon Sep 17 00:00:00 2001 From: Joshua King Date: Tue, 20 Oct 2026 13:05:32 -0400 Subject: [PATCH] Add a fleet overview page that shows every printer's status at once. Co-authored-by: Cursor --- esphome/bambu-display/README.md | 28 +- esphome/bambu-display/dashboard.h | 71 ++ esphome/bambu-display/generate.py | 161 +++- esphome/bambu-display/generated.yaml | 991 ++++++++++++++++++++++- esphome/bambu-display/preview.py | 28 +- esphome/bambu-display/test_dashboard.cpp | 37 + esphome/bambu-display/test_generate.py | 28 +- 7 files changed, 1283 insertions(+), 61 deletions(-) diff --git a/esphome/bambu-display/README.md b/esphome/bambu-display/README.md index aee6655..3de0baf 100644 --- a/esphome/bambu-display/README.md +++ b/esphome/bambu-display/README.md @@ -17,24 +17,28 @@ Home Assistant must be running and its printer integrations must have current pr The configured swipe order is: -1. PrintsCharming — P1S, Office, both four-slot AMS units. -2. Polly-Pocket — A1 mini, Office, external spool. -3. Ds-Turd-Flinger — A1, Upstairs, four-slot AMS. -4. Jordyns-Layer-Slayer — A1, Upstairs, four-slot AMS. -5. Constipation-Orion — Office, left/right nozzles and both four-slot AMS units. +1. Fleet — one row per printer: status, job name, progress, finish time and + time remaining. The header counts printers that need attention, are printing, + paused, idle, offline or still syncing. Tap a row to open that printer. +2. PrintsCharming — P1S, Office, both four-slot AMS units. +3. Polly-Pocket — A1 mini, Office, external spool. +4. Ds-Turd-Flinger — A1, Upstairs, four-slot AMS. +5. Jordyns-Layer-Slayer — A1, Upstairs, four-slot AMS. +6. Constipation-Orion — Office, left/right nozzles and both four-slot AMS units. AMS A uses `constipation_orion_ams_1`; AMS B retains its original `h2c_31b8ap5a2200288_ams_2` entity prefix. -6. Centauri Carbon — Elegoo FDM, Office, nozzle/bed current and target temperatures +7. Centauri Carbon — Elegoo FDM, Office, nozzle/bed current and target temperatures and model/auxiliary/enclosure fan speeds. -7. Saturn — Elegoo resin, Garage, progress, UV LED temperature, current/total layers, +8. Saturn — Elegoo resin, Garage, progress, UV LED temperature, current/total layers, elapsed time, remaining time and estimated finish. ![Saturn resin page with synthetic telemetry](previews/saturn.png) -Swipe left for the next printer or right for the previous printer. Navigation wraps -from the last page to the first. Previous/Next buttons provide the same navigation. -Swipes propagate through the arc gauges and filament slots. A swipe consumes the -rest of that touch so dragging across a navigation button does not also click it. +Swipe left for the next page or right for the previous page. Navigation wraps +from the last printer back to the fleet page. Previous/Next buttons provide the +same navigation. Swipes propagate through the fleet rows, arc gauges and filament +slots. A swipe consumes the rest of that touch so dragging across a row or a +navigation button does not also click it. FDM pages show print status, job name, progress, nozzle and bed current/target temperatures, remaining time, estimated finish time, current/total layers, print @@ -142,7 +146,7 @@ python esphome/bambu-display/generate.py --check The generator requires PyYAML (already included in ESPHome's Python environment). Do not hand-edit `generated.yaml`. There is no two-printer limit in the generator; -adding a printer creates its own subscriptions, widgets and swipe page. The practical +adding a printer creates its own subscriptions, widgets, swipe page and fleet row. The practical limit depends on the board's available RAM and flash, and adding printers requires regenerating and installing the firmware. Seven printers are configured; generation is also tested with nine. Each Bambu printer's filament strip supports up to eight slots. diff --git a/esphome/bambu-display/dashboard.h b/esphome/bambu-display/dashboard.h index 6003db6..f72e333 100644 --- a/esphome/bambu-display/dashboard.h +++ b/esphome/bambu-display/dashboard.h @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace bambu_display { @@ -212,6 +213,19 @@ struct Snapshot { for (char &c : text) c = static_cast(std::toupper(static_cast(c))); return text; } + std::string overview_task() const { + if (!live()) return connected ? "Waiting for printer data" : "Waiting for Home Assistant"; + return valid_text(task) ? task : "No active print"; + } + std::string overview_meta() const { + if (!live()) return "--"; + const std::string progress_text = std::isfinite(progress) + ? number(std::clamp(progress, 0.0f, 100.0f)) + "%" : "--"; + const std::string time_line = remaining(time_left, remaining_unit); + if (!finish_time.empty() && time_line != "--") + return progress_text + " " + finish_time + " " + time_line; + return progress_text + " " + time_line; + } uint32_t status_color() const { if (!live()) return MUTED; const auto state = lower(status); @@ -223,6 +237,42 @@ struct Snapshot { } }; +struct FleetCounts { + int attention = 0, printing = 0, paused = 0, idle = 0, offline = 0, syncing = 0; +}; + +inline void tally(FleetCounts &counts, const Snapshot &data) { + if (!data.connected || lower(data.online) == "off" || lower(data.online) == "false") { + ++counts.offline; + return; + } + if (!data.live()) { + ++counts.syncing; + return; + } + if (data.has_print_error() || is_on(data.hms_error) || lower(data.status) == "failed") { + ++counts.attention; + return; + } + const auto state = lower(data.status); + if (state == "running" || state == "printing") ++counts.printing; + else if (state == "pause" || state == "paused" || state == "pausing") ++counts.paused; + else ++counts.idle; +} + +inline std::string fleet_summary(const FleetCounts &counts) { + const std::pair items[] = { + {counts.attention, "attention"}, {counts.printing, "printing"}, {counts.paused, "paused"}, + {counts.idle, "idle"}, {counts.offline, "offline"}, {counts.syncing, "syncing"}}; + std::string text; + for (const auto &item : items) { + if (!item.first) continue; + if (!text.empty()) text += ", "; + text += std::to_string(item.first) + " " + item.second; + } + return text.empty() ? "Syncing" : text; +} + } // namespace bambu_display #ifdef USE_LVGL @@ -239,6 +289,9 @@ struct Widgets { GaugeWidgets uv{}, layer{}; lv_obj_t *elapsed = nullptr; }; +struct OverviewWidgets { + lv_obj_t *status, *task, *meta, *progress, *accent; +}; inline void set_text(lv_obj_t *label, const std::string &text) { if (text != lv_label_get_text(label)) lv_label_set_text(label, text.c_str()); @@ -306,5 +359,23 @@ inline void render(const Snapshot &data, const Widgets &widgets) { } } +inline void render_overview(const Snapshot &data, const OverviewWidgets &widgets) { + const bool live = data.live(); + const uint32_t color = data.status_color(); + set_text(widgets.status, data.status_text()); + lv_obj_set_style_text_color(widgets.status, lv_color_hex(color), 0); + if (widgets.task) set_text(widgets.task, data.overview_task()); + set_text(widgets.meta, data.overview_meta()); + lv_bar_set_value(widgets.progress, live ? gauge_value(data.progress, 100) : 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(widgets.progress, lv_color_hex(color), LV_PART_INDICATOR); + lv_obj_set_style_bg_color(widgets.accent, lv_color_hex(color), 0); +} + +inline void render_fleet_summary(lv_obj_t *label, const FleetCounts &counts) { + set_text(label, fleet_summary(counts)); + const uint32_t color = counts.attention ? RED : counts.printing ? GREEN : MUTED; + lv_obj_set_style_text_color(label, lv_color_hex(color), 0); +} + } // namespace bambu_display #endif diff --git a/esphome/bambu-display/generate.py b/esphome/bambu-display/generate.py index 7f163c5..f3f02dc 100644 --- a/esphome/bambu-display/generate.py +++ b/esphome/bambu-display/generate.py @@ -121,7 +121,7 @@ def fdm_gauges(printer): return widgets -def page_for(printer, index, total): +def page_for(printer, display_index, total): pid = printer['id'] resin = printer.get('profile') == 'elegoo_resin' widgets = [ @@ -129,7 +129,7 @@ def page_for(printer, index, total): 'min_value': 0, 'max_value': 100, 'value': 0, 'bg_color': TRACK, 'indicator': {'bg_color': GREEN}, 'gesture_bubble': True}}, label(printer['name'], 16, 17, 485, 26, text_color=GREEN), - label(f'{index + 1} / {total}', 520, 24, 70, 14, text_color=MUTED, text_align='CENTER'), + label(f'{display_index} / {total}', 520, 24, 70, 14, text_color=MUTED, text_align='CENTER'), label('SYNCING', 598, 21, 186, 16, id=f'{pid}_status_label', text_align='RIGHT', text_color=MUTED, clickable=True, on_click=[{'lvgl.widget.show': f'{pid}_health_panel'}]), {'obj': {'x': 578, 'y': 64, 'width': 1, 'height': 362, 'bg_color': TRACK, @@ -213,6 +213,116 @@ def page_for(printer, index, total): 'on_load': [{'script.execute': f'{pid}_refresh'}], 'widgets': widgets} +def snapshot_lines(printer, expressions, trays): + profile = printer.get('profile', 'bambu') + elegoo = profile.startswith('elegoo_') + resin = profile == 'elegoo_resin' + lines = ['bambu_display::Snapshot data;', + 'data.connected = id(ha_api).is_connected_with_state_subscription();'] + for key, expr in expressions.items(): + if not key.startswith(('tray_', 'hms_1_', 'hms_2_', 'hms_3_')): + lines.append(f'data.{key} = {expr};') + if elegoo: + lines.append('data.elegoo = true;') + else: + for number in range(1, 4): + lines.append('data.health_notices.push_back({' + ', '.join( + expressions[f'hms_{number}_{key}'] for key in ('code', 'message', 'severity')) + '});') + if resin: + lines.append('data.resin = true;') + if printer.get('dual_nozzle'): + lines.append('data.dual_nozzle = true;') + lines += [ + 'const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit);', + 'const auto clock = id(ha_time).now();', + 'if (clock.is_valid() && seconds_left >= 0)', + ' data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M");', + ] + for i, tray in enumerate(trays): + lines.append('data.trays.push_back({' + ', '.join([ + json.dumps(tray['label']), expressions[f'tray_{i}_type'], + expressions[f'tray_{i}_color'], expressions[f'tray_{i}_active']]) + '});') + return lines + + +def overview_layout(count): + row_h = (436 - 46) // count + return row_h, row_h >= 52 + + +def overview_row(printer, y, row_h, two_line): + pid = printer['id'] + name_w, status_x, status_w = (430, 448, 312) if two_line else (240, 258, 150) + meta_x, meta_w, meta_y = (448, 312, 28) if two_line else (414, 346, 8) + children = [ + {'obj': {'id': f'{pid}_overview_accent', 'x': 0, 'y': 8, 'width': 4, + 'height': max(row_h - 16, 8), 'bg_color': MUTED, 'bg_opa': 'COVER', + 'border_width': 0, 'radius': 2, 'pad_all': 0, 'scrollable': False, + 'gesture_bubble': True}}, + label(printer['name'], 12, 2 if two_line else 6, name_w, 18 if two_line else 16), + label('SYNCING', status_x, 6 if two_line else 8, status_w, 16 if two_line else 14, + id=f'{pid}_overview_status', text_align='RIGHT', text_color=MUTED), + ] + if two_line: + children.append(label('Waiting for Home Assistant', 12, 28, 430, 12, + id=f'{pid}_overview_task', text_color=MUTED)) + children += [ + label('--', meta_x, meta_y, meta_w, 12 if two_line else 14, + id=f'{pid}_overview_meta', text_align='RIGHT', text_color=MUTED), + {'bar': {'id': f'{pid}_overview_progress', 'x': 12, 'y': row_h - 7, 'width': 748, 'height': 4, + 'min_value': 0, 'max_value': 100, 'value': 0, 'bg_color': TRACK, + 'indicator': {'bg_color': GREEN}, 'gesture_bubble': True}}, + ] + return {'obj': {'id': f'{pid}_overview_row', 'x': 8, 'y': y, 'width': 784, 'height': row_h, + 'bg_color': BG, 'bg_opa': 'COVER', 'border_width': 0, 'pad_all': 0, + 'radius': 0, 'scrollable': False, 'clickable': True, 'gesture_bubble': True, + 'on_click': [{'lvgl.page.show': f'{pid}_page'}], 'widgets': children}} + + +def overview_page(printers): + row_h, two_line = overview_layout(len(printers)) + widgets = [ + label('FLEET', 16, 8, 180, 26, text_color=GREEN), + label('SYNCING', 210, 16, 574, 14, id='fleet_summary', text_align='RIGHT', text_color=MUTED), + ] + widgets += [overview_row(printer, 46 + index * row_h, row_h, two_line) + for index, printer in enumerate(printers)] + for direction, x, text in (('previous', 16, '<'), ('next', 720, '>')): + widgets.append({'button': {'x': x, 'y': 446, 'width': 64, 'height': 30, + 'bg_color': TRACK, 'radius': 8, 'border_width': 0, + 'pad_all': 0, 'gesture_bubble': True, + 'widgets': [{'label': {'text': text, 'align': 'CENTER', + 'text_color': TEXT_COLOR, 'text_font': 'montserrat_18'}}], + 'on_click': [{f'lvgl.page.{direction}': {}}]}}) + widgets.append(label(f'1 / {len(printers) + 1}', 94, 449, 612, 14, + text_align='CENTER', text_color=MUTED)) + return {'id': 'fleet_page', 'bg_color': BG, 'bg_opa': 'COVER', + 'pad_all': 0, 'scrollable': False, 'scrollbar_mode': 'OFF', + 'on_swipe_left': [ + {'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')}, + {'lvgl.page.next': {'animation': 'MOVE_LEFT', 'time': '180ms'}}], + 'on_swipe_right': [ + {'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')}, + {'lvgl.page.previous': {'animation': 'MOVE_RIGHT', 'time': '180ms'}}], + 'on_load': [{'script.execute': 'fleet_refresh'}], 'widgets': widgets} + + +def fleet_refresh_lines(snapshots): + _, two_line = overview_layout(len(snapshots)) + lines = ['bambu_display::FleetCounts counts;'] + for pid, snapshot in snapshots: + task = f'id({pid}_overview_task)' if two_line else 'nullptr' + lines.append('{') + lines.extend(snapshot) + lines.append('bambu_display::tally(counts, data);') + lines.append('bambu_display::render_overview(data, {' + ', '.join([ + f'id({pid}_overview_status)', task, f'id({pid}_overview_meta)', + f'id({pid}_overview_progress)', f'id({pid}_overview_accent)']) + '});') + lines.append('}') + lines.append('bambu_display::render_fleet_summary(id(fleet_summary), counts);') + return lines + + def color_test_page(): widgets = [label('DISPLAY COLOR TEST', 16, 18, 768, 28), label('Each block must match its label. Swipe to return to the printers.', @@ -236,6 +346,8 @@ def generate(printers): validate_printers(printers) package = {'sensor': [], 'text_sensor': [], 'script': [], 'interval': [{'interval': '1s', 'then': []}], 'lvgl': {'pages': []}} + snapshots = [] + page_total = len(printers) + 1 for index, printer in enumerate(printers): pid = printer['id'] profile = printer.get('profile', 'bambu') @@ -287,34 +399,11 @@ def generate(printers): subscription['attribute'] = attribute package['text_sensor'].append(subscription) expressions[key] = f'id({sid}).state' - lines = ['bambu_display::Snapshot data;', - 'data.connected = id(ha_api).is_connected_with_state_subscription();'] - for key, expr in expressions.items(): - if not key.startswith(('tray_', 'hms_1_', 'hms_2_', 'hms_3_')): - lines.append(f'data.{key} = {expr};') - if elegoo: - lines.append('data.elegoo = true;') - else: - for number in range(1, 4): - lines.append('data.health_notices.push_back({' + ', '.join( - expressions[f'hms_{number}_{key}'] for key in ('code', 'message', 'severity')) + '});') - if resin: - lines.append('data.resin = true;') - if printer.get('dual_nozzle'): - lines.append('data.dual_nozzle = true;') - lines += [ - 'const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit);', - 'const auto clock = id(ha_time).now();', - 'if (clock.is_valid() && seconds_left >= 0)', - ' data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M");', - ] - tray_widgets = [] - for i, tray in enumerate(trays): - lines.append('data.trays.push_back({' + ', '.join([ - json.dumps(tray['label']), expressions[f'tray_{i}_type'], - expressions[f'tray_{i}_color'], expressions[f'tray_{i}_active']]) + '});') - tray_widgets.append('{' + ', '.join(f'id({pid}_tray_{i}_{part})' - for part in ('card', 'swatch', 'label')) + '}') + lines = snapshot_lines(printer, expressions, trays) + snapshots.append((pid, list(lines))) + tray_widgets = ['{' + ', '.join(f'id({pid}_tray_{i}_{part})' + for part in ('card', 'swatch', 'label')) + '}' + for i in range(len(trays))] refs = [f'id({pid}_{key})' for key in ( 'status_label', 'task_label', 'top_progress', 'remaining_label', 'layers_label', 'stage_label', 'health_details')] @@ -331,7 +420,13 @@ def generate(printers): package['interval'][0]['then'].append({'if': { 'condition': {'lvgl.page.is_showing': f'{pid}_page'}, 'then': [{'script.execute': f'{pid}_refresh'}]}}) - package['lvgl']['pages'].append(page_for(printer, index, len(printers))) + package['lvgl']['pages'].append(page_for(printer, index + 2, page_total)) + package['script'].append({'id': 'fleet_refresh', 'then': [ + {'lambda': Lambda('\n'.join(fleet_refresh_lines(snapshots)))}]}) + package['interval'][0]['then'].append({'if': { + 'condition': {'lvgl.page.is_showing': 'fleet_page'}, + 'then': [{'script.execute': 'fleet_refresh'}]}}) + package['lvgl']['pages'].insert(0, overview_page(printers)) package['lvgl']['pages'].append(color_test_page()) package['button'] = [{'platform': 'template', 'name': 'Display Color Test', 'entity_category': 'diagnostic', @@ -351,10 +446,10 @@ def main(): if args.check: if not destination.exists() or destination.read_text() != content: raise SystemExit('generated.yaml is stale; run generate.py') - print(f'Generated configuration is current ({len(printers)} printer pages).') + print(f'Generated configuration is current ({len(printers)} printer pages plus fleet overview).') else: destination.write_text(content) - print(f'Generated {destination.name} with {len(printers)} printer pages.') + print(f'Generated {destination.name} with {len(printers)} printer pages plus fleet overview.') if __name__ == '__main__': diff --git a/esphome/bambu-display/generated.yaml b/esphome/bambu-display/generated.yaml index c783aec..57ba348 100644 --- a/esphome/bambu-display/generated.yaml +++ b/esphome/bambu-display/generated.yaml @@ -1487,6 +1487,272 @@ script: if (clock.is_valid() && seconds_left >= 0) data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M"); bambu_display::render(data, {id(saturn_status_label), id(saturn_task_label), id(saturn_top_progress), id(saturn_remaining_label), id(saturn_layers_label), id(saturn_stage_label), id(saturn_health_details), {id(saturn_progress_arc), id(saturn_progress_label), id(saturn_progress_detail)}, {}, {}, {}, {}, {}, {}, {id(saturn_uv_arc), id(saturn_uv_label), id(saturn_uv_detail)}, {id(saturn_layer_arc), id(saturn_layer_label), id(saturn_layer_detail)}, id(saturn_elapsed_label)}); +- id: fleet_refresh + then: + - lambda: !lambda |- + bambu_display::FleetCounts counts; + { + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(prints_charming_progress).state; + data.nozzle = id(prints_charming_nozzle).state; + data.nozzle_target = id(prints_charming_nozzle_target).state; + data.bed = id(prints_charming_bed).state; + data.bed_target = id(prints_charming_bed_target).state; + data.time_left = id(prints_charming_time_left).state; + data.layer = id(prints_charming_layer).state; + data.total_layers = id(prints_charming_total_layers).state; + data.fan = id(prints_charming_fan).state; + data.aux_fan = id(prints_charming_aux_fan).state; + data.exhaust_fan = id(prints_charming_exhaust_fan).state; + data.status = id(prints_charming_status).state; + data.task = id(prints_charming_task).state; + data.stage = id(prints_charming_stage).state; + data.online = id(prints_charming_online).state; + data.print_error = id(prints_charming_print_error).state; + data.hms_error = id(prints_charming_hms_error).state; + data.hms_count = id(prints_charming_hms_count).state; + data.print_error_message = id(prints_charming_print_error_message).state; + data.print_error_code = id(prints_charming_print_error_code).state; + data.remaining_unit = id(prints_charming_remaining_unit).state; + data.nozzle_unit = id(prints_charming_nozzle_unit).state; + data.bed_unit = id(prints_charming_bed_unit).state; + data.health_notices.push_back({id(prints_charming_hms_1_code).state, id(prints_charming_hms_1_message).state, id(prints_charming_hms_1_severity).state}); + data.health_notices.push_back({id(prints_charming_hms_2_code).state, id(prints_charming_hms_2_message).state, id(prints_charming_hms_2_severity).state}); + data.health_notices.push_back({id(prints_charming_hms_3_code).state, id(prints_charming_hms_3_message).state, id(prints_charming_hms_3_severity).state}); + const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); + const auto clock = id(ha_time).now(); + if (clock.is_valid() && seconds_left >= 0) + data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M"); + data.trays.push_back({"A1", id(prints_charming_tray_0_type).state, id(prints_charming_tray_0_color).state, id(prints_charming_tray_0_active).state}); + data.trays.push_back({"A2", id(prints_charming_tray_1_type).state, id(prints_charming_tray_1_color).state, id(prints_charming_tray_1_active).state}); + data.trays.push_back({"A3", id(prints_charming_tray_2_type).state, id(prints_charming_tray_2_color).state, id(prints_charming_tray_2_active).state}); + data.trays.push_back({"A4", id(prints_charming_tray_3_type).state, id(prints_charming_tray_3_color).state, id(prints_charming_tray_3_active).state}); + data.trays.push_back({"B1", id(prints_charming_tray_4_type).state, id(prints_charming_tray_4_color).state, id(prints_charming_tray_4_active).state}); + data.trays.push_back({"B2", id(prints_charming_tray_5_type).state, id(prints_charming_tray_5_color).state, id(prints_charming_tray_5_active).state}); + data.trays.push_back({"B3", id(prints_charming_tray_6_type).state, id(prints_charming_tray_6_color).state, id(prints_charming_tray_6_active).state}); + data.trays.push_back({"B4", id(prints_charming_tray_7_type).state, id(prints_charming_tray_7_color).state, id(prints_charming_tray_7_active).state}); + bambu_display::tally(counts, data); + bambu_display::render_overview(data, {id(prints_charming_overview_status), id(prints_charming_overview_task), id(prints_charming_overview_meta), id(prints_charming_overview_progress), id(prints_charming_overview_accent)}); + } + { + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(polly_pocket_progress).state; + data.nozzle = id(polly_pocket_nozzle).state; + data.nozzle_target = id(polly_pocket_nozzle_target).state; + data.bed = id(polly_pocket_bed).state; + data.bed_target = id(polly_pocket_bed_target).state; + data.time_left = id(polly_pocket_time_left).state; + data.layer = id(polly_pocket_layer).state; + data.total_layers = id(polly_pocket_total_layers).state; + data.fan = id(polly_pocket_fan).state; + data.status = id(polly_pocket_status).state; + data.task = id(polly_pocket_task).state; + data.stage = id(polly_pocket_stage).state; + data.online = id(polly_pocket_online).state; + data.print_error = id(polly_pocket_print_error).state; + data.hms_error = id(polly_pocket_hms_error).state; + data.hms_count = id(polly_pocket_hms_count).state; + data.print_error_message = id(polly_pocket_print_error_message).state; + data.print_error_code = id(polly_pocket_print_error_code).state; + data.remaining_unit = id(polly_pocket_remaining_unit).state; + data.nozzle_unit = id(polly_pocket_nozzle_unit).state; + data.bed_unit = id(polly_pocket_bed_unit).state; + data.health_notices.push_back({id(polly_pocket_hms_1_code).state, id(polly_pocket_hms_1_message).state, id(polly_pocket_hms_1_severity).state}); + data.health_notices.push_back({id(polly_pocket_hms_2_code).state, id(polly_pocket_hms_2_message).state, id(polly_pocket_hms_2_severity).state}); + data.health_notices.push_back({id(polly_pocket_hms_3_code).state, id(polly_pocket_hms_3_message).state, id(polly_pocket_hms_3_severity).state}); + const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); + const auto clock = id(ha_time).now(); + if (clock.is_valid() && seconds_left >= 0) + data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M"); + data.trays.push_back({"EXT", id(polly_pocket_tray_0_type).state, id(polly_pocket_tray_0_color).state, id(polly_pocket_tray_0_active).state}); + bambu_display::tally(counts, data); + bambu_display::render_overview(data, {id(polly_pocket_overview_status), id(polly_pocket_overview_task), id(polly_pocket_overview_meta), id(polly_pocket_overview_progress), id(polly_pocket_overview_accent)}); + } + { + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(ds_turd_flinger_progress).state; + data.nozzle = id(ds_turd_flinger_nozzle).state; + data.nozzle_target = id(ds_turd_flinger_nozzle_target).state; + data.bed = id(ds_turd_flinger_bed).state; + data.bed_target = id(ds_turd_flinger_bed_target).state; + data.time_left = id(ds_turd_flinger_time_left).state; + data.layer = id(ds_turd_flinger_layer).state; + data.total_layers = id(ds_turd_flinger_total_layers).state; + data.fan = id(ds_turd_flinger_fan).state; + data.status = id(ds_turd_flinger_status).state; + data.task = id(ds_turd_flinger_task).state; + data.stage = id(ds_turd_flinger_stage).state; + data.online = id(ds_turd_flinger_online).state; + data.print_error = id(ds_turd_flinger_print_error).state; + data.hms_error = id(ds_turd_flinger_hms_error).state; + data.hms_count = id(ds_turd_flinger_hms_count).state; + data.print_error_message = id(ds_turd_flinger_print_error_message).state; + data.print_error_code = id(ds_turd_flinger_print_error_code).state; + data.remaining_unit = id(ds_turd_flinger_remaining_unit).state; + data.nozzle_unit = id(ds_turd_flinger_nozzle_unit).state; + data.bed_unit = id(ds_turd_flinger_bed_unit).state; + data.health_notices.push_back({id(ds_turd_flinger_hms_1_code).state, id(ds_turd_flinger_hms_1_message).state, id(ds_turd_flinger_hms_1_severity).state}); + data.health_notices.push_back({id(ds_turd_flinger_hms_2_code).state, id(ds_turd_flinger_hms_2_message).state, id(ds_turd_flinger_hms_2_severity).state}); + data.health_notices.push_back({id(ds_turd_flinger_hms_3_code).state, id(ds_turd_flinger_hms_3_message).state, id(ds_turd_flinger_hms_3_severity).state}); + const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); + const auto clock = id(ha_time).now(); + if (clock.is_valid() && seconds_left >= 0) + data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M"); + data.trays.push_back({"A1", id(ds_turd_flinger_tray_0_type).state, id(ds_turd_flinger_tray_0_color).state, id(ds_turd_flinger_tray_0_active).state}); + data.trays.push_back({"A2", id(ds_turd_flinger_tray_1_type).state, id(ds_turd_flinger_tray_1_color).state, id(ds_turd_flinger_tray_1_active).state}); + data.trays.push_back({"A3", id(ds_turd_flinger_tray_2_type).state, id(ds_turd_flinger_tray_2_color).state, id(ds_turd_flinger_tray_2_active).state}); + data.trays.push_back({"A4", id(ds_turd_flinger_tray_3_type).state, id(ds_turd_flinger_tray_3_color).state, id(ds_turd_flinger_tray_3_active).state}); + bambu_display::tally(counts, data); + bambu_display::render_overview(data, {id(ds_turd_flinger_overview_status), id(ds_turd_flinger_overview_task), id(ds_turd_flinger_overview_meta), id(ds_turd_flinger_overview_progress), id(ds_turd_flinger_overview_accent)}); + } + { + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(jordyns_layer_slayer_progress).state; + data.nozzle = id(jordyns_layer_slayer_nozzle).state; + data.nozzle_target = id(jordyns_layer_slayer_nozzle_target).state; + data.bed = id(jordyns_layer_slayer_bed).state; + data.bed_target = id(jordyns_layer_slayer_bed_target).state; + data.time_left = id(jordyns_layer_slayer_time_left).state; + data.layer = id(jordyns_layer_slayer_layer).state; + data.total_layers = id(jordyns_layer_slayer_total_layers).state; + data.fan = id(jordyns_layer_slayer_fan).state; + data.status = id(jordyns_layer_slayer_status).state; + data.task = id(jordyns_layer_slayer_task).state; + data.stage = id(jordyns_layer_slayer_stage).state; + data.online = id(jordyns_layer_slayer_online).state; + data.print_error = id(jordyns_layer_slayer_print_error).state; + data.hms_error = id(jordyns_layer_slayer_hms_error).state; + data.hms_count = id(jordyns_layer_slayer_hms_count).state; + data.print_error_message = id(jordyns_layer_slayer_print_error_message).state; + data.print_error_code = id(jordyns_layer_slayer_print_error_code).state; + data.remaining_unit = id(jordyns_layer_slayer_remaining_unit).state; + data.nozzle_unit = id(jordyns_layer_slayer_nozzle_unit).state; + data.bed_unit = id(jordyns_layer_slayer_bed_unit).state; + data.health_notices.push_back({id(jordyns_layer_slayer_hms_1_code).state, id(jordyns_layer_slayer_hms_1_message).state, id(jordyns_layer_slayer_hms_1_severity).state}); + data.health_notices.push_back({id(jordyns_layer_slayer_hms_2_code).state, id(jordyns_layer_slayer_hms_2_message).state, id(jordyns_layer_slayer_hms_2_severity).state}); + data.health_notices.push_back({id(jordyns_layer_slayer_hms_3_code).state, id(jordyns_layer_slayer_hms_3_message).state, id(jordyns_layer_slayer_hms_3_severity).state}); + const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); + const auto clock = id(ha_time).now(); + if (clock.is_valid() && seconds_left >= 0) + data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M"); + data.trays.push_back({"A1", id(jordyns_layer_slayer_tray_0_type).state, id(jordyns_layer_slayer_tray_0_color).state, id(jordyns_layer_slayer_tray_0_active).state}); + data.trays.push_back({"A2", id(jordyns_layer_slayer_tray_1_type).state, id(jordyns_layer_slayer_tray_1_color).state, id(jordyns_layer_slayer_tray_1_active).state}); + data.trays.push_back({"A3", id(jordyns_layer_slayer_tray_2_type).state, id(jordyns_layer_slayer_tray_2_color).state, id(jordyns_layer_slayer_tray_2_active).state}); + data.trays.push_back({"A4", id(jordyns_layer_slayer_tray_3_type).state, id(jordyns_layer_slayer_tray_3_color).state, id(jordyns_layer_slayer_tray_3_active).state}); + bambu_display::tally(counts, data); + bambu_display::render_overview(data, {id(jordyns_layer_slayer_overview_status), id(jordyns_layer_slayer_overview_task), id(jordyns_layer_slayer_overview_meta), id(jordyns_layer_slayer_overview_progress), id(jordyns_layer_slayer_overview_accent)}); + } + { + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(constipation_orion_progress).state; + data.nozzle = id(constipation_orion_nozzle).state; + data.nozzle_target = id(constipation_orion_nozzle_target).state; + data.bed = id(constipation_orion_bed).state; + data.bed_target = id(constipation_orion_bed_target).state; + data.time_left = id(constipation_orion_time_left).state; + data.layer = id(constipation_orion_layer).state; + data.total_layers = id(constipation_orion_total_layers).state; + data.fan = id(constipation_orion_fan).state; + data.left_nozzle = id(constipation_orion_left_nozzle).state; + data.left_target = id(constipation_orion_left_target).state; + data.right_nozzle = id(constipation_orion_right_nozzle).state; + data.right_target = id(constipation_orion_right_target).state; + data.exhaust_fan = id(constipation_orion_exhaust_fan).state; + data.status = id(constipation_orion_status).state; + data.task = id(constipation_orion_task).state; + data.stage = id(constipation_orion_stage).state; + data.online = id(constipation_orion_online).state; + data.print_error = id(constipation_orion_print_error).state; + data.hms_error = id(constipation_orion_hms_error).state; + data.hms_count = id(constipation_orion_hms_count).state; + data.print_error_message = id(constipation_orion_print_error_message).state; + data.print_error_code = id(constipation_orion_print_error_code).state; + data.remaining_unit = id(constipation_orion_remaining_unit).state; + data.nozzle_unit = id(constipation_orion_nozzle_unit).state; + data.bed_unit = id(constipation_orion_bed_unit).state; + data.health_notices.push_back({id(constipation_orion_hms_1_code).state, id(constipation_orion_hms_1_message).state, id(constipation_orion_hms_1_severity).state}); + data.health_notices.push_back({id(constipation_orion_hms_2_code).state, id(constipation_orion_hms_2_message).state, id(constipation_orion_hms_2_severity).state}); + data.health_notices.push_back({id(constipation_orion_hms_3_code).state, id(constipation_orion_hms_3_message).state, id(constipation_orion_hms_3_severity).state}); + data.dual_nozzle = true; + const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); + const auto clock = id(ha_time).now(); + if (clock.is_valid() && seconds_left >= 0) + data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M"); + data.trays.push_back({"A1", id(constipation_orion_tray_0_type).state, id(constipation_orion_tray_0_color).state, id(constipation_orion_tray_0_active).state}); + data.trays.push_back({"A2", id(constipation_orion_tray_1_type).state, id(constipation_orion_tray_1_color).state, id(constipation_orion_tray_1_active).state}); + data.trays.push_back({"A3", id(constipation_orion_tray_2_type).state, id(constipation_orion_tray_2_color).state, id(constipation_orion_tray_2_active).state}); + data.trays.push_back({"A4", id(constipation_orion_tray_3_type).state, id(constipation_orion_tray_3_color).state, id(constipation_orion_tray_3_active).state}); + data.trays.push_back({"B1", id(constipation_orion_tray_4_type).state, id(constipation_orion_tray_4_color).state, id(constipation_orion_tray_4_active).state}); + data.trays.push_back({"B2", id(constipation_orion_tray_5_type).state, id(constipation_orion_tray_5_color).state, id(constipation_orion_tray_5_active).state}); + data.trays.push_back({"B3", id(constipation_orion_tray_6_type).state, id(constipation_orion_tray_6_color).state, id(constipation_orion_tray_6_active).state}); + data.trays.push_back({"B4", id(constipation_orion_tray_7_type).state, id(constipation_orion_tray_7_color).state, id(constipation_orion_tray_7_active).state}); + bambu_display::tally(counts, data); + bambu_display::render_overview(data, {id(constipation_orion_overview_status), id(constipation_orion_overview_task), id(constipation_orion_overview_meta), id(constipation_orion_overview_progress), id(constipation_orion_overview_accent)}); + } + { + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(centauri_carbon_progress).state; + data.time_left = id(centauri_carbon_time_left).state; + data.layer = id(centauri_carbon_layer).state; + data.total_layers = id(centauri_carbon_total_layers).state; + data.nozzle = id(centauri_carbon_nozzle).state; + data.bed = id(centauri_carbon_bed).state; + data.nozzle_target = id(centauri_carbon_nozzle_target).state; + data.bed_target = id(centauri_carbon_bed_target).state; + data.fan = id(centauri_carbon_fan).state; + data.aux_fan = id(centauri_carbon_aux_fan).state; + data.exhaust_fan = id(centauri_carbon_exhaust_fan).state; + data.status = id(centauri_carbon_status).state; + data.task = id(centauri_carbon_task).state; + data.stage = id(centauri_carbon_stage).state; + data.print_error = id(centauri_carbon_print_error).state; + data.print_error_reason = id(centauri_carbon_print_error_reason).state; + data.online = id(centauri_carbon_online).state; + data.remaining_unit = id(centauri_carbon_remaining_unit).state; + data.nozzle_unit = id(centauri_carbon_nozzle_unit).state; + data.bed_unit = id(centauri_carbon_bed_unit).state; + data.elegoo = true; + const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); + const auto clock = id(ha_time).now(); + if (clock.is_valid() && seconds_left >= 0) + data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M"); + bambu_display::tally(counts, data); + bambu_display::render_overview(data, {id(centauri_carbon_overview_status), id(centauri_carbon_overview_task), id(centauri_carbon_overview_meta), id(centauri_carbon_overview_progress), id(centauri_carbon_overview_accent)}); + } + { + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(saturn_progress).state; + data.time_left = id(saturn_time_left).state; + data.layer = id(saturn_layer).state; + data.total_layers = id(saturn_total_layers).state; + data.uv = id(saturn_uv).state; + data.elapsed = id(saturn_elapsed).state; + data.status = id(saturn_status).state; + data.task = id(saturn_task).state; + data.stage = id(saturn_stage).state; + data.print_error = id(saturn_print_error).state; + data.print_error_reason = id(saturn_print_error_reason).state; + data.online = id(saturn_online).state; + data.remaining_unit = id(saturn_remaining_unit).state; + data.uv_unit = id(saturn_uv_unit).state; + data.elapsed_unit = id(saturn_elapsed_unit).state; + data.elegoo = true; + data.resin = true; + const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); + const auto clock = id(ha_time).now(); + if (clock.is_valid() && seconds_left >= 0) + data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M"); + bambu_display::tally(counts, data); + bambu_display::render_overview(data, {id(saturn_overview_status), id(saturn_overview_task), id(saturn_overview_meta), id(saturn_overview_progress), id(saturn_overview_accent)}); + } + bambu_display::render_fleet_summary(id(fleet_summary), counts); interval: - interval: 1s then: @@ -1525,8 +1791,719 @@ interval: lvgl.page.is_showing: saturn_page then: - script.execute: saturn_refresh + - if: + condition: + lvgl.page.is_showing: fleet_page + then: + - script.execute: fleet_refresh lvgl: pages: + - id: fleet_page + bg_color: 0 + bg_opa: COVER + pad_all: 0 + scrollable: false + scrollbar_mode: 'OFF' + on_swipe_left: + - lambda: !lambda |- + if (auto *input = lv_indev_active()) lv_indev_wait_release(input); + - lvgl.page.next: + animation: MOVE_LEFT + time: 180ms + on_swipe_right: + - lambda: !lambda |- + if (auto *input = lv_indev_active()) lv_indev_wait_release(input); + - lvgl.page.previous: + animation: MOVE_RIGHT + time: 180ms + on_load: + - script.execute: fleet_refresh + widgets: + - label: + text: FLEET + x: 16 + y: 8 + width: 180 + height: 34 + text_color: 65280 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + - label: + text: SYNCING + x: 210 + y: 16 + width: 574 + height: 22 + text_color: 11449271 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + id: fleet_summary + text_align: RIGHT + - obj: + id: prints_charming_overview_row + x: 8 + y: 46 + width: 784 + height: 55 + bg_color: 0 + bg_opa: COVER + border_width: 0 + pad_all: 0 + radius: 0 + scrollable: false + clickable: true + gesture_bubble: true + on_click: + - lvgl.page.show: prints_charming_page + widgets: + - obj: + id: prints_charming_overview_accent + x: 0 + y: 8 + width: 4 + height: 39 + bg_color: 11449271 + bg_opa: COVER + border_width: 0 + radius: 2 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: PrintsCharming + x: 12 + y: 2 + width: 430 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + - label: + text: SYNCING + x: 448 + y: 6 + width: 312 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: prints_charming_overview_status + text_align: RIGHT + - label: + text: Waiting for Home Assistant + x: 12 + y: 28 + width: 430 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_overview_task + - label: + text: -- + x: 448 + y: 28 + width: 312 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_overview_meta + text_align: RIGHT + - bar: + id: prints_charming_overview_progress + x: 12 + y: 48 + width: 748 + height: 4 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - obj: + id: polly_pocket_overview_row + x: 8 + y: 101 + width: 784 + height: 55 + bg_color: 0 + bg_opa: COVER + border_width: 0 + pad_all: 0 + radius: 0 + scrollable: false + clickable: true + gesture_bubble: true + on_click: + - lvgl.page.show: polly_pocket_page + widgets: + - obj: + id: polly_pocket_overview_accent + x: 0 + y: 8 + width: 4 + height: 39 + bg_color: 11449271 + bg_opa: COVER + border_width: 0 + radius: 2 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: Polly-Pocket + x: 12 + y: 2 + width: 430 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + - label: + text: SYNCING + x: 448 + y: 6 + width: 312 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_overview_status + text_align: RIGHT + - label: + text: Waiting for Home Assistant + x: 12 + y: 28 + width: 430 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_overview_task + - label: + text: -- + x: 448 + y: 28 + width: 312 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_overview_meta + text_align: RIGHT + - bar: + id: polly_pocket_overview_progress + x: 12 + y: 48 + width: 748 + height: 4 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - obj: + id: ds_turd_flinger_overview_row + x: 8 + y: 156 + width: 784 + height: 55 + bg_color: 0 + bg_opa: COVER + border_width: 0 + pad_all: 0 + radius: 0 + scrollable: false + clickable: true + gesture_bubble: true + on_click: + - lvgl.page.show: ds_turd_flinger_page + widgets: + - obj: + id: ds_turd_flinger_overview_accent + x: 0 + y: 8 + width: 4 + height: 39 + bg_color: 11449271 + bg_opa: COVER + border_width: 0 + radius: 2 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: Ds-Turd-Flinger + x: 12 + y: 2 + width: 430 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + - label: + text: SYNCING + x: 448 + y: 6 + width: 312 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_overview_status + text_align: RIGHT + - label: + text: Waiting for Home Assistant + x: 12 + y: 28 + width: 430 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_overview_task + - label: + text: -- + x: 448 + y: 28 + width: 312 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_overview_meta + text_align: RIGHT + - bar: + id: ds_turd_flinger_overview_progress + x: 12 + y: 48 + width: 748 + height: 4 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - obj: + id: jordyns_layer_slayer_overview_row + x: 8 + y: 211 + width: 784 + height: 55 + bg_color: 0 + bg_opa: COVER + border_width: 0 + pad_all: 0 + radius: 0 + scrollable: false + clickable: true + gesture_bubble: true + on_click: + - lvgl.page.show: jordyns_layer_slayer_page + widgets: + - obj: + id: jordyns_layer_slayer_overview_accent + x: 0 + y: 8 + width: 4 + height: 39 + bg_color: 11449271 + bg_opa: COVER + border_width: 0 + radius: 2 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: Jordyns-Layer-Slayer + x: 12 + y: 2 + width: 430 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + - label: + text: SYNCING + x: 448 + y: 6 + width: 312 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_overview_status + text_align: RIGHT + - label: + text: Waiting for Home Assistant + x: 12 + y: 28 + width: 430 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_overview_task + - label: + text: -- + x: 448 + y: 28 + width: 312 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_overview_meta + text_align: RIGHT + - bar: + id: jordyns_layer_slayer_overview_progress + x: 12 + y: 48 + width: 748 + height: 4 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - obj: + id: constipation_orion_overview_row + x: 8 + y: 266 + width: 784 + height: 55 + bg_color: 0 + bg_opa: COVER + border_width: 0 + pad_all: 0 + radius: 0 + scrollable: false + clickable: true + gesture_bubble: true + on_click: + - lvgl.page.show: constipation_orion_page + widgets: + - obj: + id: constipation_orion_overview_accent + x: 0 + y: 8 + width: 4 + height: 39 + bg_color: 11449271 + bg_opa: COVER + border_width: 0 + radius: 2 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: Constipation-Orion + x: 12 + y: 2 + width: 430 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + - label: + text: SYNCING + x: 448 + y: 6 + width: 312 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_overview_status + text_align: RIGHT + - label: + text: Waiting for Home Assistant + x: 12 + y: 28 + width: 430 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_overview_task + - label: + text: -- + x: 448 + y: 28 + width: 312 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_overview_meta + text_align: RIGHT + - bar: + id: constipation_orion_overview_progress + x: 12 + y: 48 + width: 748 + height: 4 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - obj: + id: centauri_carbon_overview_row + x: 8 + y: 321 + width: 784 + height: 55 + bg_color: 0 + bg_opa: COVER + border_width: 0 + pad_all: 0 + radius: 0 + scrollable: false + clickable: true + gesture_bubble: true + on_click: + - lvgl.page.show: centauri_carbon_page + widgets: + - obj: + id: centauri_carbon_overview_accent + x: 0 + y: 8 + width: 4 + height: 39 + bg_color: 11449271 + bg_opa: COVER + border_width: 0 + radius: 2 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: Centauri Carbon + x: 12 + y: 2 + width: 430 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + - label: + text: SYNCING + x: 448 + y: 6 + width: 312 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_overview_status + text_align: RIGHT + - label: + text: Waiting for Home Assistant + x: 12 + y: 28 + width: 430 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_overview_task + - label: + text: -- + x: 448 + y: 28 + width: 312 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_overview_meta + text_align: RIGHT + - bar: + id: centauri_carbon_overview_progress + x: 12 + y: 48 + width: 748 + height: 4 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - obj: + id: saturn_overview_row + x: 8 + y: 376 + width: 784 + height: 55 + bg_color: 0 + bg_opa: COVER + border_width: 0 + pad_all: 0 + radius: 0 + scrollable: false + clickable: true + gesture_bubble: true + on_click: + - lvgl.page.show: saturn_page + widgets: + - obj: + id: saturn_overview_accent + x: 0 + y: 8 + width: 4 + height: 39 + bg_color: 11449271 + bg_opa: COVER + border_width: 0 + radius: 2 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: Saturn + x: 12 + y: 2 + width: 430 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + - label: + text: SYNCING + x: 448 + y: 6 + width: 312 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: saturn_overview_status + text_align: RIGHT + - label: + text: Waiting for Home Assistant + x: 12 + y: 28 + width: 430 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: saturn_overview_task + - label: + text: -- + x: 448 + y: 28 + width: 312 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: saturn_overview_meta + text_align: RIGHT + - bar: + id: saturn_overview_progress + x: 12 + y: 48 + width: 748 + height: 4 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - button: + x: 16 + y: 446 + width: 64 + height: 30 + bg_color: 1647403 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: < + align: CENTER + text_color: 15658734 + text_font: montserrat_18 + on_click: + - lvgl.page.previous: {} + - button: + x: 720 + y: 446 + width: 64 + height: 30 + bg_color: 1647403 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: '>' + align: CENTER + text_color: 15658734 + text_font: montserrat_18 + on_click: + - lvgl.page.next: {} + - label: + text: 1 / 8 + x: 94 + y: 449 + width: 612 + height: 22 + text_color: 11449271 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER - id: prints_charming_page bg_color: 0 bg_opa: COVER @@ -1572,7 +2549,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 1 / 7 + text: 2 / 8 x: 520 y: 24 width: 70 @@ -2461,7 +3438,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 2 / 7 + text: 3 / 8 x: 520 y: 24 width: 70 @@ -3059,7 +4036,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 3 / 7 + text: 4 / 8 x: 520 y: 24 width: 70 @@ -3777,7 +4754,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 4 / 7 + text: 5 / 8 x: 520 y: 24 width: 70 @@ -4495,7 +5472,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 5 / 7 + text: 6 / 8 x: 520 y: 24 width: 70 @@ -5384,7 +6361,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 6 / 7 + text: 7 / 8 x: 520 y: 24 width: 70 @@ -5953,7 +6930,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 7 / 7 + text: 8 / 8 x: 520 y: 24 width: 70 diff --git a/esphome/bambu-display/preview.py b/esphome/bambu-display/preview.py index 3579c3b..9e7be8a 100644 --- a/esphome/bambu-display/preview.py +++ b/esphome/bambu-display/preview.py @@ -87,7 +87,19 @@ inline void setup() { ''') steps = [{'component.update': entry['id']} for section in ('sensor', 'text_sensor') for entry in package[section]] - steps += [{'delay': '1s'}] + steps += [{'delay': '1s'}, + {'lvgl.page.show': 'fleet_page'}, + {'script.execute': 'fleet_refresh'}, {'delay': '300ms'}, + {'snapshot.take': {'id': 'panel_display', 'filename': 'fleet.bmp'}}, + {'lambda': Lambda( + 'if (std::string(lv_label_get_text(id(fleet_summary))) != ' + + json.dumps(f'{len(printers)} printing') + + ') { ESP_LOGE("preview", "Fleet summary render failed"); exit(7); }')}] + for printer in printers: + pid = printer['id'] + steps.append({'lambda': Lambda( + f'if (std::string(lv_label_get_text(id({pid}_overview_status))) != "PRINTING") ' + f'{{ ESP_LOGE("preview", "Fleet row render failed: {pid}"); exit(7); }}')}) for printer in printers: steps += [{'lvgl.page.show': printer['id'] + '_page'}, {'script.execute': printer['id'] + '_refresh'}, {'delay': '300ms'}, @@ -125,7 +137,7 @@ inline void setup() { {'lvgl.widget.hide': demo_printer + '_health_panel'}, {'text_sensor.template.publish': {'id': demo_printer + '_hms_error', 'state': 'off'}}, {'script.execute': demo_printer + '_refresh'}] - steps += [{'lvgl.page.show': printers[0]['id'] + '_page'}, {'delay': '250ms'}, + steps += [{'lvgl.page.show': 'fleet_page'}, {'delay': '250ms'}, {'lambda': Lambda('bambu_preview::setup();')}] for x, y, expect_hidden in [(690, 30, False), (700, 394, True)]: steps += [ @@ -136,11 +148,13 @@ inline void setup() { if len(printers) > 1: # First -> second -> first -> last -> first, then a swipe starting # on NEXT must advance exactly once even when released over PREV. - gestures = [(650, 180, 180, printers[1]['id']), - (180, 650, 180, printers[0]['id']), + # Fleet -> first printer -> fleet -> last printer -> fleet, then a + # swipe starting on NEXT advances exactly once. + gestures = [(650, 180, 180, printers[0]['id']), + (180, 650, 180, 'fleet'), (180, 650, 180, printers[-1]['id']), - (650, 180, 180, printers[0]['id']), - (730, 60, 460, printers[1]['id'])] + (650, 180, 180, 'fleet'), + (730, 60, 460, printers[0]['id'])] for start, end, y, expected in gestures: steps += [ {'lambda': Lambda(f'bambu_preview::x = {start}; bambu_preview::y = {y}; bambu_preview::pressed = true;')}, @@ -165,7 +179,7 @@ inline void setup() { subprocess.run(['esphome', 'compile', str(filename)], check=True) binary = root / '.esphome/build/bambu-display-preview/.pioenvs/bambu-display-preview/program' subprocess.run([str(binary)], check=True, timeout=30, cwd=root) - for name in [printer['id'] for printer in printers] + ['color_test', 'health_notice']: + for name in ['fleet'] + [printer['id'] for printer in printers] + ['color_test', 'health_notice']: bmp = root / '.esphome/snapshots/bambu-display-preview' / (name + '.bmp') with Image.open(bmp) as image: image.save(args.output / (name + '.png')) diff --git a/esphome/bambu-display/test_dashboard.cpp b/esphome/bambu-display/test_dashboard.cpp index 0147d12..6d17d1e 100644 --- a/esphome/bambu-display/test_dashboard.cpp +++ b/esphome/bambu-display/test_dashboard.cpp @@ -103,4 +103,41 @@ int main() { assert(!data.live()); assert(data.status_text() == "PRINTER OFFLINE"); assert(data.health_details().find("unavailable") != std::string::npos); + + data.connected = true; + data.online = "on"; + data.elegoo = false; + data.print_error = "off"; + data.print_error_reason = "ok"; + data.hms_error = "off"; + data.status = "running"; + data.task = "Workshop organizer"; + data.progress = 67; + data.time_left = 1.75f; + data.remaining_unit = "h"; + data.finish_time = "16:05"; + assert(data.overview_task() == "Workshop organizer"); + assert(data.overview_meta() == "67% 16:05 1h 45m"); + data.task = ""; + assert(data.overview_task() == "No active print"); + data.connected = false; + assert(data.overview_task() == "Waiting for Home Assistant"); + assert(data.overview_meta() == "--"); + + FleetCounts counts; + data.connected = true; + data.online = "on"; + data.status = "printing"; + tally(counts, data); + data.status = "pause"; + tally(counts, data); + data.print_error = "on"; + tally(counts, data); + data.print_error = "off"; + data.online = "off"; + tally(counts, data); + data.online = "unavailable"; + data.connected = true; + tally(counts, data); + assert(fleet_summary(counts) == "1 attention, 1 printing, 1 paused, 1 offline, 1 syncing"); } diff --git a/esphome/bambu-display/test_generate.py b/esphome/bambu-display/test_generate.py index 9397d38..fb8d38d 100644 --- a/esphome/bambu-display/test_generate.py +++ b/esphome/bambu-display/test_generate.py @@ -27,8 +27,8 @@ class FleetTests(unittest.TestCase): extra.update(id=f'printer_{index}', entity_prefix=f'new_printer_{index}') self.printers.append(extra) package = generate(self.printers) - self.assertEqual(len([page for page in package['lvgl']['pages'] if not page.get('skip')]), expected_count) - self.assertEqual(len(package['script']), expected_count) + self.assertEqual(len([page for page in package['lvgl']['pages'] if not page.get('skip')]), expected_count + 1) + self.assertEqual(len(package['script']), expected_count + 1) subscriptions = {entry['id']: entry['entity_id'] for entry in package['sensor']} self.assertEqual(subscriptions['printer_9_progress'], 'sensor.new_printer_9_print_progress') for page in package['lvgl']['pages']: @@ -147,6 +147,30 @@ class FleetTests(unittest.TestCase): self.assertEqual(config['packages']['fleet'], 'bambu-display/generated.yaml') self.assertEqual(len(names), 2) + def test_fleet_overview_lists_every_printer(self): + package = generate(self.printers) + page = package['lvgl']['pages'][0] + self.assertEqual(page['id'], 'fleet_page') + self.assertFalse(page.get('skip')) + rows = [widget['obj'] for widget in page['widgets'] + if 'obj' in widget and str(widget['obj'].get('id', '')).endswith('_overview_row')] + self.assertEqual([row['id'][:-len('_overview_row')] for row in rows], + [printer['id'] for printer in self.printers]) + previous = 46 + for row, printer in zip(rows, self.printers): + self.assertEqual(row['on_click'], [{'lvgl.page.show': printer['id'] + '_page'}]) + self.assertGreaterEqual(row['y'], previous) + self.assertLessEqual(row['y'] + row['height'], 440) + previous = row['y'] + row['height'] + code = next(script['then'][0]['lambda'] for script in package['script'] if script['id'] == 'fleet_refresh') + for printer in self.printers: + self.assertIn(f'id({printer["id"]}_overview_status)', code) + shown = [step['if']['condition']['lvgl.page.is_showing'] for step in package['interval'][0]['then']] + self.assertIn('fleet_page', shown) + charming = next(item for item in package['lvgl']['pages'] if item['id'] == 'prints_charming_page') + numbers = [next(iter(widget.values())).get('text') for widget in charming['widgets']] + self.assertIn(f'2 / {len(self.printers) + 1}', numbers) + def test_diagnostic_color_page_is_skipped_during_normal_navigation(self): package = generate(self.printers) page = next(page for page in package['lvgl']['pages'] if page['id'] == 'color_test_page')