Fix Orion AMS mapping and show printer health details
@@ -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
|
||||||
|
|||||||
@@ -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 ¬ice : 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 ¬ice : 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 ¬ice : 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 ¬ice : 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];
|
||||||
|
|||||||
@@ -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) + '}')
|
||||||
|
|||||||
@@ -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__':
|
||||||
|
|||||||
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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'}]):
|
||||||
|
|||||||