Add a fleet overview page that shows every printer's status at once.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace bambu_display {
|
||||
@@ -212,6 +213,19 @@ struct Snapshot {
|
||||
for (char &c : text) c = static_cast<char>(std::toupper(static_cast<unsigned char>(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<int, const char *> 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
|
||||
|
||||
@@ -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__':
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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'))
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user