Fix Orion AMS mapping and show printer health details

This commit is contained in:
Joshua King
2026-10-18 09:29:07 -04:00
parent f672de484e
commit 3c940f8027
15 changed files with 1104 additions and 24 deletions
+24 -5
View File
@@ -21,7 +21,9 @@ The configured swipe order is:
2. Polly-Pocket — A1 mini, Office, external spool. 2. Polly-Pocket — A1 mini, Office, external spool.
3. Ds-Turd-Flinger — A1, Upstairs, four-slot AMS. 3. Ds-Turd-Flinger — A1, Upstairs, four-slot AMS.
4. Jordyns-Layer-Slayer — A1, Upstairs, four-slot AMS. 4. Jordyns-Layer-Slayer — A1, Upstairs, four-slot AMS.
5. Constipation-Orion — Office, left/right nozzles and four-slot AMS. 5. 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.
Swipe left for the next printer or right for the previous printer. Navigation wraps 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. from the last page to the first. Previous/Next buttons provide the same navigation.
@@ -34,9 +36,25 @@ stage, fan speeds and filament colors/types. The layout follows BambuHelper's
classic gauges: black background, bright green accents, six arc gauges and a classic gauges: black background, bright green accents, six arc gauges and a
vertical AMS column. Nozzle arcs are orange and the bed arc is cyan. For dual-nozzle vertical AMS column. Nozzle arcs are orange and the bed arc is cyan. For dual-nozzle
printers the auxiliary-fan position becomes the right-nozzle gauge. The active printers the auxiliary-fan position becomes the right-nozzle gauge. The active
filament slot has a green outline. Print errors filament slot has a green outline. Print errors use red; paused prints use amber.
and HMS alerts change the status badge to red; paused prints use amber. The display
shows an alert badge rather than the detailed HMS diagnostic text. **HMS means Health Management System.** Its binary sensor means the Bambu integration
has one or more printer health notifications, which can include information or
maintenance messages as well as faults. Tap the status in the upper-right corner
or the scrolling message above the footer to open the health-details panel. It
shows the codes, severities and descriptions reported by Home Assistant, for up to
three current HMS notices, plus print-error details. Scroll vertically for long
messages and tap Close to return. If more than three notices exist, the panel says
how many additional notices are available in Home Assistant.
The status uses **HEALTH INFO** (cyan) for information-only notifications,
**HEALTH NOTICE** (amber) for common/unknown severities or incomplete details, and
**HEALTH ERROR** (red) for fatal/serious severities. Severity comes from the Bambu
integration's `N-Severity` attributes. Missing or unloaded messages never establish
an information-only status. The display does not dismiss or clear printer alerts.
The [`hms_errors` sensor attributes](https://github.com/greghesp/ha-bambulab/blob/main/custom_components/bambu_lab/pybambu/models.py)
provide `Count`, `1-Code`, `1-Error`, `1-Severity`, and the corresponding numbered
fields for subsequent notices. Stale attributes are ignored when the sensor is off.
When Home Assistant disconnects or a printer is offline, its live metrics are cleared When Home Assistant disconnects or a printer is offline, its live metrics are cleared
to `--` rather than continuing to show cached values as live. Unknown individual to `--` rather than continuing to show cached values as live. Unknown individual
@@ -140,7 +158,8 @@ python esphome/bambu-display/preview.py
``` ```
`preview.py` compiles the same generated LVGL widgets for ESPHome's SDL host with `preview.py` compiles the same generated LVGL widgets for ESPHome's SDL host with
clearly synthetic telemetry, then captures all five pages into `previews/`. It needs clearly synthetic telemetry, then captures all five pages and a synthetic health
notice into `previews/`. The demo notice is not a diagnosis of any real printer. It needs
ESPHome, Pillow, a C++ compiler and SDL2 (`sdl2-config` on PATH). It runs headlessly, ESPHome, Pillow, a C++ compiler and SDL2 (`sdl2-config` on PATH). It runs headlessly,
without connecting to Home Assistant or any printer. It also injects LVGL pointer without connecting to Home Assistant or any printer. It also injects LVGL pointer
samples to check left/right swipes, page wraparound and consumption of a swipe that samples to check left/right swipes, page wraparound and consumption of a swipe that
+71 -3
View File
@@ -5,6 +5,7 @@
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include <cstdint> #include <cstdint>
#include <cstdlib>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -99,9 +100,13 @@ struct Tray {
std::string label, type, color, active; std::string label, type, color, active;
}; };
struct HealthNotice { std::string code, message, severity; };
struct Snapshot { struct Snapshot {
bool connected = false; bool connected = false;
std::string online, status, task, stage, print_error, hms_error; std::string online, status, task, stage, print_error, hms_error;
std::string hms_count, print_error_message, print_error_code;
std::vector<HealthNotice> health_notices;
std::string remaining_unit, nozzle_unit, bed_unit; std::string remaining_unit, nozzle_unit, bed_unit;
std::string finish_time; std::string finish_time;
float progress = NAN, nozzle = NAN, nozzle_target = NAN, bed = NAN, bed_target = NAN; float progress = NAN, nozzle = NAN, nozzle_target = NAN, bed = NAN, bed_target = NAN;
@@ -112,12 +117,70 @@ struct Snapshot {
std::vector<Tray> trays; std::vector<Tray> trays;
bool live() const { return connected && is_on(online) && valid_text(status); } bool live() const { return connected && is_on(online) && valid_text(status); }
int notice_count() const {
int loaded = 0;
for (const auto &notice : health_notices)
if (valid_text(notice.code) || valid_text(notice.message)) ++loaded;
char *end;
const long count = std::strtol(hms_count.c_str(), &end, 10);
return end != hms_count.c_str() && *end == '\0' && count >= 0 && count < 10000
? std::max(loaded, static_cast<int>(count)) : loaded;
}
int health_severity_rank() const {
int rank = 1, loaded = 0;
for (const auto &notice : health_notices) {
if (!valid_text(notice.code) && !valid_text(notice.message)) continue;
++loaded;
const auto severity = lower(notice.severity);
rank = std::max(rank, severity == "fatal" || severity == "serious" ? 3 : severity == "info" ? 1 : 2);
}
// Missing/unloaded messages cannot establish an info-only notification.
return loaded == 0 || notice_count() > loaded ? std::max(rank, 2) : rank;
}
std::string alert_summary() const {
if (is_on(print_error))
return valid_text(print_error_message) ? print_error_message : "Print error reported; tap status for details";
if (is_on(hms_error)) {
for (const auto &notice : health_notices)
if (valid_text(notice.message)) return notice.message;
return "Printer health notification; tap status for details";
}
return "";
}
std::string health_details() const {
if (!live()) return "Printer health data is unavailable. Waiting for a live connection.";
std::string details;
if (is_on(print_error)) {
details = "PRINT ERROR\n";
if (valid_text(print_error_code)) details += print_error_code + "\n";
details += valid_text(print_error_message) ? print_error_message : "Message not supplied by Home Assistant.";
details += "\n\n";
}
if (is_on(hms_error)) {
details += "HMS = Health Management System\n";
int loaded = 0;
for (const auto &notice : health_notices) {
if (!valid_text(notice.code) && !valid_text(notice.message)) continue;
details += "\nNotice " + std::to_string(++loaded);
if (valid_text(notice.severity)) details += " / " + notice.severity;
details += "\n";
if (valid_text(notice.code)) details += notice.code + "\n";
details += valid_text(notice.message) ? notice.message : "Message not supplied by Home Assistant.";
details += "\n";
}
if (loaded == 0) details += "\nNotification is active; details have not arrived from Home Assistant.\n";
if (notice_count() > loaded)
details += "\n" + std::to_string(notice_count() - loaded) + " more notification(s). View all in Home Assistant.\n";
}
return details.empty() ? "No current printer health notifications." : details;
}
std::string status_text() const { std::string status_text() const {
if (!connected) return "HA OFFLINE"; if (!connected) return "HA OFFLINE";
if (lower(online) == "off" || lower(online) == "false") return "PRINTER OFFLINE"; if (lower(online) == "off" || lower(online) == "false") return "PRINTER OFFLINE";
if (!live()) return "SYNCING"; if (!live()) return "SYNCING";
if (is_on(print_error)) return "PRINT ERROR"; if (is_on(print_error)) return "PRINT ERROR";
if (is_on(hms_error)) return "HMS ALERT"; if (is_on(hms_error))
return health_severity_rank() >= 3 ? "HEALTH ERROR" : health_severity_rank() == 1 ? "HEALTH INFO" : "HEALTH NOTICE";
const auto state = lower(status); const auto state = lower(status);
if (state == "running") return "PRINTING"; if (state == "running") return "PRINTING";
if (state == "pause" || state == "paused") return "PAUSED"; if (state == "pause" || state == "paused") return "PAUSED";
@@ -129,7 +192,9 @@ struct Snapshot {
uint32_t status_color() const { uint32_t status_color() const {
if (!live()) return MUTED; if (!live()) return MUTED;
const auto state = lower(status); const auto state = lower(status);
if (is_on(print_error) || is_on(hms_error) || state == "failed") return RED; if (is_on(print_error) || state == "failed") return RED;
if (is_on(hms_error))
return health_severity_rank() >= 3 ? RED : health_severity_rank() == 1 ? 0x00E5E5 : AMBER;
if (state == "pause" || state == "paused") return AMBER; if (state == "pause" || state == "paused") return AMBER;
return GREEN; return GREEN;
} }
@@ -145,7 +210,7 @@ namespace bambu_display {
struct TrayWidgets { lv_obj_t *card, *swatch, *label; }; struct TrayWidgets { lv_obj_t *card, *swatch, *label; };
struct GaugeWidgets { lv_obj_t *arc, *value, *detail; }; struct GaugeWidgets { lv_obj_t *arc, *value, *detail; };
struct Widgets { struct Widgets {
lv_obj_t *status, *task, *top_progress, *remaining, *layers, *stage; lv_obj_t *status, *task, *top_progress, *remaining, *layers, *stage, *health_details;
GaugeWidgets progress, nozzle, bed, fan, aux, exhaust; GaugeWidgets progress, nozzle, bed, fan, aux, exhaust;
std::vector<TrayWidgets> trays; std::vector<TrayWidgets> trays;
}; };
@@ -192,7 +257,10 @@ inline void render(const Snapshot &data, const Widgets &widgets) {
set_text(widgets.remaining, time_line); set_text(widgets.remaining, time_line);
set_text(widgets.layers, live ? "Layer " + number(data.layer) + " / " + number(data.total_layers) : "--"); set_text(widgets.layers, live ? "Layer " + number(data.layer) + " / " + number(data.total_layers) : "--");
std::string stage = live ? humanize(data.stage) : "Telemetry unavailable"; std::string stage = live ? humanize(data.stage) : "Telemetry unavailable";
if (live && (is_on(data.print_error) || is_on(data.hms_error))) stage = data.alert_summary();
set_text(widgets.stage, stage); set_text(widgets.stage, stage);
lv_obj_set_style_text_color(widgets.stage, lv_color_hex(live && !data.alert_summary().empty() ? color : MUTED), 0);
set_text(widgets.health_details, data.health_details());
for (size_t i = 0; i < std::min(data.trays.size(), widgets.trays.size()); ++i) { for (size_t i = 0; i < std::min(data.trays.size(), widgets.trays.size()); ++i) {
const auto &tray = data.trays[i]; const auto &tray = data.trays[i];
const auto &widget = widgets.trays[i]; const auto &widget = widgets.trays[i];
+35 -4
View File
@@ -104,7 +104,8 @@ def page_for(printer, index, total):
'indicator': {'bg_color': GREEN}, 'gesture_bubble': True}}, 'indicator': {'bg_color': GREEN}, 'gesture_bubble': True}},
label(printer['name'], 16, 17, 485, 26, text_color=GREEN), 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'{index + 1} / {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), 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, {'obj': {'x': 578, 'y': 64, 'width': 1, 'height': 362, 'bg_color': TRACK,
'border_width': 0, 'pad_all': 0, 'scrollable': False, 'gesture_bubble': True}}, 'border_width': 0, 'pad_all': 0, 'scrollable': False, 'gesture_bubble': True}},
] ]
@@ -119,7 +120,9 @@ def page_for(printer, index, total):
widgets += gauge(pid, 'exhaust', 'Exhaust', 388, 229, GREEN, 100) widgets += gauge(pid, 'exhaust', 'Exhaust', 388, 229, GREEN, 100)
widgets += [ widgets += [
label('--', 16, 401, 544, 26, id=f'{pid}_remaining_label', text_color=GREEN, text_align='CENTER'), label('--', 16, 401, 544, 26, id=f'{pid}_remaining_label', text_color=GREEN, text_align='CENTER'),
label('Telemetry unavailable', 16, 430, 544, 12, id=f'{pid}_stage_label', text_color=MUTED, text_align='CENTER'), label('Telemetry unavailable', 16, 430, 544, 12, id=f'{pid}_stage_label', text_color=MUTED,
text_align='CENTER', long_mode='SCROLL_CIRCULAR', clickable=True,
on_click=[{'lvgl.widget.show': f'{pid}_health_panel'}]),
label('--', 594, 397, 190, 16, id=f'{pid}_layers_label', text_color=MUTED, text_align='CENTER'), label('--', 594, 397, 190, 16, id=f'{pid}_layers_label', text_color=MUTED, text_align='CENTER'),
] ]
trays = trays_for(printer) trays = trays_for(printer)
@@ -153,6 +156,22 @@ def page_for(printer, index, total):
'on_click': [{f'lvgl.page.{direction}': {}}]}}) 'on_click': [{f'lvgl.page.{direction}': {}}]}})
widgets.append(label('Waiting for Home Assistant', 94, 449, 612, 14, widgets.append(label('Waiting for Home Assistant', 94, 449, 612, 14,
id=f'{pid}_task_label', text_align='CENTER')) id=f'{pid}_task_label', text_align='CENTER'))
widgets.append({'obj': {
'id': f'{pid}_health_panel', 'x': 24, 'y': 62, 'width': 752, 'height': 366,
'hidden': True, 'bg_color': BG, 'bg_opa': 'COVER', 'border_width': 2,
'border_color': MUTED, 'radius': 10, 'pad_all': 0, 'scrollable': False,
'gesture_bubble': False,
'widgets': [
label('Printer health / ' + printer['name'], 16, 14, 716, 20),
{'obj': {'x': 12, 'y': 50, 'width': 724, 'height': 246,
'bg_color': BG, 'border_width': 0, 'pad_all': 0,
'scroll_dir': 'VER', 'scrollbar_mode': 'AUTO', 'gesture_bubble': False,
'widgets': [label('No current health notifications.', 0, 0, 700, 18,
id=f'{pid}_health_details', long_mode='WRAP', height='SIZE_CONTENT')]}},
{'button': {'x': 624, 'y': 310, 'width': 108, 'height': 40, 'bg_color': TRACK,
'radius': 6, 'widgets': [{'label': {'text': 'CLOSE', 'align': 'CENTER'}}],
'on_click': [{'lvgl.widget.hide': f'{pid}_health_panel'}]}},
]}})
return {'id': f'{pid}_page', 'bg_color': BG, 'bg_opa': 'COVER', return {'id': f'{pid}_page', 'bg_color': BG, 'bg_opa': 'COVER',
'pad_all': 0, 'scrollable': False, 'scrollbar_mode': 'OFF', 'pad_all': 0, 'scrollable': False, 'scrollbar_mode': 'OFF',
'on_swipe_left': [ 'on_swipe_left': [
@@ -203,6 +222,14 @@ def generate(printers):
text_fields = {key: (entity(printer, key, suffix), None) for key, suffix in TEXT.items()} text_fields = {key: (entity(printer, key, suffix), None) for key, suffix in TEXT.items()}
text_fields.update({key: (entity(printer, key, suffix, 'binary_sensor'), None) text_fields.update({key: (entity(printer, key, suffix, 'binary_sensor'), None)
for key, suffix in BINARY.items()}) for key, suffix in BINARY.items()})
hms_entity = entity(printer, 'hms_error', BINARY['hms_error'], 'binary_sensor')
text_fields['hms_count'] = (hms_entity, 'Count')
for number in range(1, 4):
for key, attribute in [('message', 'Error'), ('code', 'Code'), ('severity', 'Severity')]:
text_fields[f'hms_{number}_{key}'] = (hms_entity, f'{number}-{attribute}')
print_error_entity = entity(printer, 'print_error', BINARY['print_error'], 'binary_sensor')
text_fields['print_error_message'] = (print_error_entity, 'error')
text_fields['print_error_code'] = (print_error_entity, 'code')
for key, numeric_key in [('remaining_unit', 'time_left'), ('nozzle_unit', 'nozzle'), ('bed_unit', 'bed')]: for key, numeric_key in [('remaining_unit', 'time_left'), ('nozzle_unit', 'nozzle'), ('bed_unit', 'bed')]:
text_fields[key] = (entity(printer, numeric_key, NUMERIC[numeric_key]), 'unit_of_measurement') text_fields[key] = (entity(printer, numeric_key, NUMERIC[numeric_key]), 'unit_of_measurement')
trays = trays_for(printer) trays = trays_for(printer)
@@ -219,8 +246,11 @@ def generate(printers):
lines = ['bambu_display::Snapshot data;', lines = ['bambu_display::Snapshot data;',
'data.connected = id(ha_api).is_connected_with_state_subscription();'] 'data.connected = id(ha_api).is_connected_with_state_subscription();']
for key, expr in expressions.items(): for key, expr in expressions.items():
if not key.startswith('tray_'): if not key.startswith(('tray_', 'hms_1_', 'hms_2_', 'hms_3_')):
lines.append(f'data.{key} = {expr};') lines.append(f'data.{key} = {expr};')
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 printer.get('dual_nozzle'): if printer.get('dual_nozzle'):
lines.append('data.dual_nozzle = true;') lines.append('data.dual_nozzle = true;')
lines += [ lines += [
@@ -237,7 +267,8 @@ def generate(printers):
tray_widgets.append('{' + ', '.join(f'id({pid}_tray_{i}_{part})' tray_widgets.append('{' + ', '.join(f'id({pid}_tray_{i}_{part})'
for part in ('card', 'swatch', 'label')) + '}') for part in ('card', 'swatch', 'label')) + '}')
refs = [f'id({pid}_{key})' for key in ( refs = [f'id({pid}_{key})' for key in (
'status_label', 'task_label', 'top_progress', 'remaining_label', 'layers_label', 'stage_label')] 'status_label', 'task_label', 'top_progress', 'remaining_label', 'layers_label', 'stage_label',
'health_details')]
for key in ('progress', 'nozzle', 'bed', 'fan', 'aux', 'exhaust'): for key in ('progress', 'nozzle', 'bed', 'fan', 'aux', 'exhaust'):
refs.append('{' + ', '.join(f'id({pid}_{key}_{part})' for part in ('arc', 'label', 'detail')) + '}') refs.append('{' + ', '.join(f'id({pid}_{key}_{part})' for part in ('arc', 'label', 'detail')) + '}')
refs.append('{' + ', '.join(tray_widgets) + '}') refs.append('{' + ', '.join(tray_widgets) + '}')
File diff suppressed because it is too large Load Diff
+23 -1
View File
@@ -25,6 +25,7 @@ def main():
'right_nozzle': 37, 'right_target': 0} 'right_nozzle': 37, 'right_target': 0}
demo_text = {'status': 'running', 'task': 'Workshop organizer - plate 2', demo_text = {'status': 'running', 'task': 'Workshop organizer - plate 2',
'stage': 'printing', 'online': 'on', 'print_error': 'off', 'hms_error': 'off', 'stage': 'printing', 'online': 'on', 'print_error': 'off', 'hms_error': 'off',
'hms_count': '0', 'print_error_message': '', 'print_error_code': '',
'remaining_unit': 'h', 'nozzle_unit': '°C', 'bed_unit': '°C'} 'remaining_unit': 'h', 'nozzle_unit': '°C', 'bed_unit': '°C'}
colors = ['#60DE9AFF', '#F4C96BFF', '#74A9E8FF', '#F8F4ECFF', colors = ['#60DE9AFF', '#F4C96BFF', '#74A9E8FF', '#F8F4ECFF',
'#DF8BBFFF', '#FA936BFF', '#8E79CAFF', '#4CC9C1FF'] '#DF8BBFFF', '#FA936BFF', '#8E79CAFF', '#4CC9C1FF']
@@ -42,6 +43,8 @@ def main():
_, index, attribute = key.split('_') _, index, attribute = key.split('_')
text = {'type': 'PLA', 'color': colors[int(index) % len(colors)], text = {'type': 'PLA', 'color': colors[int(index) % len(colors)],
'active': 'on' if index == '0' else 'off'}[attribute] 'active': 'on' if index == '0' else 'off'}[attribute]
elif key.startswith(('hms_1_', 'hms_2_', 'hms_3_')):
text = ''
else: else:
text = demo_text[key] text = demo_text[key]
value = 'std::string(' + json.dumps(text, ensure_ascii=False) + ')' value = 'std::string(' + json.dumps(text, ensure_ascii=False) + ')'
@@ -85,8 +88,26 @@ inline void setup() {
{'snapshot.take': {'id': 'panel_display', 'filename': printer['id'] + '.bmp'}}] {'snapshot.take': {'id': 'panel_display', 'filename': printer['id'] + '.bmp'}}]
steps += [{'lvgl.page.show': 'color_test_page'}, {'delay': '300ms'}, steps += [{'lvgl.page.show': 'color_test_page'}, {'delay': '300ms'},
{'snapshot.take': {'id': 'panel_display', 'filename': 'color_test.bmp'}}] {'snapshot.take': {'id': 'panel_display', 'filename': 'color_test.bmp'}}]
demo_printer = printers[0]['id']
for key, value in {'hms_error': 'on', 'hms_count': '1', 'hms_1_code': 'DEMO_CODE',
'hms_1_severity': 'common',
'hms_1_message': 'DEMO: A printer maintenance notification. This is synthetic preview data, not an actual fault diagnosis.'}.items():
steps.append({'text_sensor.template.publish': {'id': f'{demo_printer}_{key}', 'state': value}})
steps += [{'lvgl.page.show': demo_printer + '_page'},
{'script.execute': demo_printer + '_refresh'},
{'lvgl.widget.show': demo_printer + '_health_panel'}, {'delay': '300ms'},
{'snapshot.take': {'id': 'panel_display', 'filename': 'health_notice.bmp'}},
{'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': printers[0]['id'] + '_page'}, {'delay': '250ms'},
{'lambda': Lambda('bambu_preview::setup();')}] {'lambda': Lambda('bambu_preview::setup();')}]
for x, y, expect_hidden in [(690, 30, False), (700, 394, True)]:
steps += [
{'lambda': Lambda(f'bambu_preview::x = {x}; bambu_preview::y = {y}; bambu_preview::pressed = true;')},
{'delay': '70ms'}, {'lambda': Lambda('bambu_preview::pressed = false;')}, {'delay': '150ms'},
{'lambda': Lambda(f'if (lv_obj_has_flag(id({demo_printer}_health_panel), LV_OBJ_FLAG_HIDDEN) != {str(expect_hidden).lower()}) {{ ESP_LOGE("preview", "Health panel tap failed"); exit(3); }}')},
]
if len(printers) > 1: if len(printers) > 1:
# First -> second -> first -> last -> first, then a swipe starting # First -> second -> first -> last -> first, then a swipe starting
# on NEXT must advance exactly once even when released over PREV. # on NEXT must advance exactly once even when released over PREV.
@@ -119,12 +140,13 @@ inline void setup() {
subprocess.run(['esphome', 'compile', str(filename)], check=True) subprocess.run(['esphome', 'compile', str(filename)], check=True)
binary = root / '.esphome/build/bambu-display-preview/.pioenvs/bambu-display-preview/program' binary = root / '.esphome/build/bambu-display-preview/.pioenvs/bambu-display-preview/program'
subprocess.run([str(binary)], check=True, timeout=30, cwd=root) subprocess.run([str(binary)], check=True, timeout=30, cwd=root)
for name in [printer['id'] for printer in printers] + ['color_test']: for name in [printer['id'] for printer in printers] + ['color_test', 'health_notice']:
bmp = root / '.esphome/snapshots/bambu-display-preview' / (name + '.bmp') bmp = root / '.esphome/snapshots/bambu-display-preview' / (name + '.bmp')
with Image.open(bmp) as image: with Image.open(bmp) as image:
image.save(args.output / (name + '.png')) image.save(args.output / (name + '.png'))
print(f'Saved {len(printers)} demo screenshots to {args.output}') print(f'Saved {len(printers)} demo screenshots to {args.output}')
print('Passed LVGL pointer swipe tests, including wraparound and button gesture consumption.') print('Passed LVGL pointer swipe tests, including wraparound and button gesture consumption.')
print('Passed status-tap health panel open and Close-button tests.')
if __name__ == '__main__': if __name__ == '__main__':
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

+3
View File
@@ -46,3 +46,6 @@ printers:
filament_banks: filament_banks:
- label: A - label: A
entity_prefix: constipation_orion_ams_1 entity_prefix: constipation_orion_ams_1
- label: B
# This unit retains its original hardware-based HA entity names.
entity_prefix: h2c_31b8ap5a2200288_ams_2
+17 -1
View File
@@ -50,9 +50,25 @@ int main() {
assert(data.status_color() == RED); assert(data.status_color() == RED);
data.print_error = "off"; data.print_error = "off";
data.hms_error = "True"; data.hms_error = "True";
assert(data.status_text() == "HMS ALERT"); assert(data.status_text() == "HEALTH NOTICE");
assert(data.status_color() == AMBER);
assert(data.health_details().find("have not arrived") != std::string::npos);
data.hms_count = "1";
data.health_notices = {{"DEMO_CODE", "A synthetic information message", "info"}};
assert(data.status_text() == "HEALTH INFO");
assert(data.health_details().find("DEMO_CODE") != std::string::npos);
assert(data.alert_summary() == "A synthetic information message");
data.health_notices.push_back({"DEMO_2", "A synthetic serious message", "serious"});
data.hms_count = "2";
assert(data.status_text() == "HEALTH ERROR");
assert(data.status_color() == RED); assert(data.status_color() == RED);
assert(data.health_details().find("A synthetic serious message") != std::string::npos);
data.health_notices.pop_back();
data.hms_count = "5";
assert(data.status_text() == "HEALTH NOTICE");
assert(data.health_details().find("4 more") != std::string::npos);
data.hms_error = "off"; data.hms_error = "off";
assert(data.health_details() == "No current printer health notifications.");
data.status = "finish"; data.status = "finish";
assert(data.status_text() == "COMPLETE"); assert(data.status_text() == "COMPLETE");
assert(data.status_color() == GREEN); assert(data.status_color() == GREEN);
+16
View File
@@ -41,6 +41,22 @@ class FleetTests(unittest.TestCase):
entry = next(row for row in package[domain] if row['id'] == f'prints_charming_{key}') entry = next(row for row in package[domain] if row['id'] == f'prints_charming_{key}')
self.assertEqual(entry['entity_id'], 'sensor.custom_remaining') self.assertEqual(entry['entity_id'], 'sensor.custom_remaining')
def test_orion_has_both_ams_units_with_their_actual_entity_prefixes(self):
package = generate(self.printers)
types = [entry['entity_id'] for entry in package['text_sensor']
if entry['id'].startswith('constipation_orion_tray_') and entry.get('attribute') == 'type']
self.assertEqual(len(types), 8)
self.assertIn('sensor.constipation_orion_ams_1_tray_1', types)
self.assertIn('sensor.h2c_31b8ap5a2200288_ams_2_tray_4', types)
def test_health_messages_import_numbered_ha_attributes(self):
package = generate(self.printers)
subscriptions = {entry['id']: entry for entry in package['text_sensor']}
message = subscriptions['prints_charming_hms_1_message']
self.assertEqual(message['entity_id'], 'binary_sensor.p1s_01p09c470102673_hms_errors')
self.assertEqual(message['attribute'], '1-Error')
self.assertEqual(subscriptions['prints_charming_hms_3_severity']['attribute'], '3-Severity')
def test_reject_empty_duplicate_and_malformed_printer_ids(self): def test_reject_empty_duplicate_and_malformed_printer_ids(self):
for invalid in ([], self.printers + [self.printers[0]], for invalid in ([], self.printers + [self.printers[0]],
[{**self.printers[0], 'id': 'invalid-id'}]): [{**self.printers[0], 'id': 'invalid-id'}]):