diff --git a/esphome/bambu-display/README.md b/esphome/bambu-display/README.md index 2215300..edd4aaf 100644 --- a/esphome/bambu-display/README.md +++ b/esphome/bambu-display/README.md @@ -1,4 +1,4 @@ -# Bambu fleet display +# Bambu and Elegoo fleet display ESPHome/LVGL monitor for the **Sunton ESP32-8048S043C**, with an 800×480 landscape RGB display, GT911 capacitive touch, 16 MB flash and 8 MB octal PSRAM. The hardware pins @@ -6,10 +6,10 @@ and LCD timings follow the [ESPHome device reference](https://devices.esphome.io This configuration is for the **C** capacitive-touch version. Inspired by [BambuHelper](https://github.com/Keralots/BambuHelper), this implementation -subscribes to the existing Home Assistant Bambu Lab entities over a single encrypted -ESPHome API connection. Home Assistant handles the printer connections. No Bambu +subscribes to the existing Home Assistant Bambu Lab and Elegoo Printer entities over +a single encrypted ESPHome API connection. Home Assistant handles the printer connections. No Bambu access codes, cloud credentials or additional Home Assistant automations are needed. -Home Assistant must be running and its Bambu integration must have current printer data. +Home Assistant must be running and its printer integrations must have current printer data. ## Pages and navigation @@ -24,13 +24,19 @@ The configured swipe order is: 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. +6. 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, + 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. -Each page shows print status, job name, progress, nozzle and bed current/target +FDM pages show print status, job name, progress, nozzle and bed current/target temperatures, remaining time, estimated finish time, current/total layers, print stage, fan speeds and filament colors/types. The layout follows BambuHelper's classic gauges: black background, bright green accents, six arc gauges and a @@ -47,6 +53,12 @@ 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. +Elegoo pages use the same status-tap details panel for `print_error` and +`error_status_reason` enum values. `none` and `ok` are normal states; unknown or +unavailable errors do not imply a fault. Elegoo pages use `sdcp_status` for printer +connectivity and do not subscribe to Bambu HMS or AMS entities. The Saturn page +omits nozzle, bed, fan and filament widgets. + 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 @@ -58,7 +70,7 @@ fields for subsequent notices. Stale attributes are ignored when the sensor is o 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 -values also show `--`. Remaining time respects the HA sensor's `h`, `min` or `s` +values also show `--`. Remaining time respects the HA sensor's `h`, `min`, `s` or `ms` unit, and temperature labels use the imported HA temperature unit. Only the visible page is refreshed each second; every printer's subscriptions remain connected. Auxiliary and exhaust fans are imported only for printers with those sensors; @@ -125,8 +137,8 @@ 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 limit depends on the board's available RAM and flash, and adding printers requires -regenerating and installing the firmware. Five printers are configured; generation -is also tested with seven. Each printer's filament strip supports up to eight slots. +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. ```yaml - id: another_printer @@ -148,6 +160,29 @@ For a dual-nozzle printer add `dual_nozzle: true`. For external filament add exist; their mappings are in `EXTRA_FANS`. A remaining-time override also updates its unit subscription. Filament entries use the Bambu integration's `type`, `color` and `active` attributes. +For Elegoo printers, set `profile: elegoo_fdm` or `profile: elegoo_resin` (the default +profile is `bambu`). These profiles map progress to `percent_complete`, job name to +`file_name`, remaining time to `remaining_print_time`, and total layers to +`total_layers`. The FDM profile reads targets from the HA `number` entities +`target_nozzle_temp` and `target_bed_temp`; it only monitors their state. Per-metric +`entities` overrides support `sensor`, `binary_sensor` and `number` domains. +Sensor types, duration units and enum states follow the +[Elegoo integration definitions](https://github.com/danielcherubini/elegoo-homeassistant/blob/main/custom_components/elegoo_printer/definitions.py). + +The configured Elegoo prefixes are `centauri_carbon` and `saturn`, matching the +repository entity snapshot. Some older automations refer to `saturn_iv`; if your +live integration still uses that prefix, change the Saturn `entity_prefix` before +regenerating. If individual entities have different names in your integration +version, use `entities` overrides, for example: + +```yaml + entities: + print_error_reason: sensor.saturn_current_print_error_status_reason +``` + +No new HA helpers are required. `dual_nozzle`, filament slots and `extra_fans` are +Bambu options; the Elegoo FDM profile includes its three fan subscriptions. + ## Development checks and screenshots ```sh @@ -158,10 +193,11 @@ python esphome/bambu-display/preview.py ``` `preview.py` compiles the same generated LVGL widgets for ESPHome's SDL host with -clearly synthetic telemetry, then captures all five pages and a synthetic health +clearly synthetic telemetry, then captures all seven 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, 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 starts on a navigation button. These are software layout previews; physical panel timings, touch and Wi-Fi still need a hardware check. +The preview also checks Elegoo telemetry, error text and clearing of offline data. diff --git a/esphome/bambu-display/dashboard.h b/esphome/bambu-display/dashboard.h index 8143e1e..6003db6 100644 --- a/esphome/bambu-display/dashboard.h +++ b/esphome/bambu-display/dashboard.h @@ -32,6 +32,11 @@ inline bool is_on(const std::string &text) { return value == "on" || value == "true" || value == "1"; } +inline bool elegoo_fault(const std::string &text) { + const auto value = lower(text); + return valid_text(value) && value != "ok" && value != "0" && value != "off" && value != "false"; +} + inline std::string humanize(std::string text) { if (!valid_text(text)) return "--"; std::replace(text.begin(), text.end(), '_', ' '); @@ -60,6 +65,7 @@ inline std::string remaining(float value, const std::string &unit) { if (unit == "h") minutes = double(value) * 60; else if (unit == "min") minutes = value; else if (unit == "s") minutes = double(value) / 60; + else if (unit == "ms") minutes = double(value) / 60000; else return "--"; // Never silently interpret hours as minutes. if (minutes > 1000000) return "--"; const auto rounded = static_cast(std::ceil(minutes - 0.001)); @@ -74,6 +80,7 @@ inline int duration_seconds(float value, const std::string &unit) { if (unit == "h") seconds = double(value) * 3600; else if (unit == "min") seconds = double(value) * 60; else if (unit == "s") seconds = value; + else if (unit == "ms") seconds = double(value) / 1000; else return -1; return seconds <= 60000000 ? static_cast(std::round(seconds)) : -1; } @@ -106,6 +113,9 @@ struct Snapshot { bool connected = false; std::string online, status, task, stage, print_error, hms_error; std::string hms_count, print_error_message, print_error_code; + std::string print_error_reason, uv_unit, elapsed_unit; + bool elegoo = false, resin = false; + float uv = NAN, elapsed = NAN; std::vector health_notices; std::string remaining_unit, nozzle_unit, bed_unit; std::string finish_time; @@ -117,6 +127,20 @@ struct Snapshot { std::vector trays; bool live() const { return connected && is_on(online) && valid_text(status); } + bool has_print_error() const { + if (!elegoo) return is_on(print_error); + return elegoo_fault(print_error) || elegoo_fault(print_error_reason) || lower(status) == "error"; + } + std::string error_message() const { + if (!elegoo) return valid_text(print_error_message) ? print_error_message : "Print error reported; tap status for details"; + std::string message; + for (const auto &text : {print_error, print_error_reason}) { + if (!elegoo_fault(text)) continue; + if (!message.empty()) message += " / "; + message += humanize(text); + } + return message.empty() ? "Print error reported; tap status for details" : message; + } int notice_count() const { int loaded = 0; for (const auto ¬ice : health_notices) @@ -138,8 +162,7 @@ struct Snapshot { 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 (has_print_error()) return error_message(); if (is_on(hms_error)) { for (const auto ¬ice : health_notices) if (valid_text(notice.message)) return notice.message; @@ -150,10 +173,10 @@ struct Snapshot { 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)) { + if (has_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 += error_message(); details += "\n\n"; } if (is_on(hms_error)) { @@ -178,13 +201,13 @@ struct Snapshot { if (!connected) return "HA OFFLINE"; if (lower(online) == "off" || lower(online) == "false") return "PRINTER OFFLINE"; if (!live()) return "SYNCING"; - if (is_on(print_error)) return "PRINT ERROR"; + if (has_print_error()) return "PRINT ERROR"; 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); - if (state == "running") return "PRINTING"; + if (state == "running" || state == "printing") return "PRINTING"; if (state == "pause" || state == "paused") return "PAUSED"; - if (state == "finish" || state == "finished") return "COMPLETE"; + if (state == "finish" || state == "finished" || state == "complete" || state == "completed") return "COMPLETE"; auto text = humanize(status); for (char &c : text) c = static_cast(std::toupper(static_cast(c))); return text; @@ -192,10 +215,10 @@ struct Snapshot { uint32_t status_color() const { if (!live()) return MUTED; const auto state = lower(status); - if (is_on(print_error) || state == "failed") return RED; + if (has_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" || state == "pausing") return AMBER; return GREEN; } }; @@ -213,6 +236,8 @@ struct Widgets { lv_obj_t *status, *task, *top_progress, *remaining, *layers, *stage, *health_details; GaugeWidgets progress, nozzle, bed, fan, aux, exhaust; std::vector trays; + GaugeWidgets uv{}, layer{}; + lv_obj_t *elapsed = nullptr; }; inline void set_text(lv_obj_t *label, const std::string &text) { @@ -238,26 +263,37 @@ inline void render(const Snapshot &data, const Widgets &widgets) { live ? remaining(data.time_left, data.remaining_unit) : "--"); set_text(widgets.task, live ? (valid_text(data.task) ? data.task : "No active print") : data.connected ? "Waiting for printer data" : "Waiting for Home Assistant"); - const float nozzle = data.dual_nozzle ? data.left_nozzle : data.nozzle; - const float target = data.dual_nozzle ? data.left_target : data.nozzle_target; - render_gauge(widgets.nozzle, live ? nozzle : NAN, live ? celsius(nozzle, data.nozzle_unit) : NAN, - 300, live ? "/ " + number(target) + " " + data.nozzle_unit : "--"); - render_gauge(widgets.bed, live ? data.bed : NAN, live ? celsius(data.bed, data.bed_unit) : NAN, - 120, live ? "/ " + number(data.bed_target) + " " + data.bed_unit : "--"); - render_gauge(widgets.fan, live ? data.fan : NAN, live ? data.fan : NAN, 100, "%"); - if (data.dual_nozzle) - render_gauge(widgets.aux, live ? data.right_nozzle : NAN, - live ? celsius(data.right_nozzle, data.nozzle_unit) : NAN, - 300, live ? "/ " + number(data.right_target) + " " + data.nozzle_unit : "--"); - else - render_gauge(widgets.aux, live ? data.aux_fan : NAN, live ? data.aux_fan : NAN, 100, "%"); - render_gauge(widgets.exhaust, live ? data.exhaust_fan : NAN, live ? data.exhaust_fan : NAN, 100, "%"); + if (data.resin) { + render_gauge(widgets.uv, live ? data.uv : NAN, + live ? celsius(data.uv, data.uv_unit) : NAN, 100, + live && valid_text(data.uv_unit) ? data.uv_unit : "--"); + const float layer_percent = std::isfinite(data.layer) && std::isfinite(data.total_layers) + && data.layer >= 0 && data.total_layers > 0 ? 100 * data.layer / data.total_layers : NAN; + render_gauge(widgets.layer, live ? data.layer : NAN, live ? layer_percent : NAN, 100, + live ? "/ " + number(data.total_layers) : "--"); + set_text(widgets.elapsed, "Elapsed: " + (live ? remaining(data.elapsed, data.elapsed_unit) : "--")); + } else { + const float nozzle = data.dual_nozzle ? data.left_nozzle : data.nozzle; + const float target = data.dual_nozzle ? data.left_target : data.nozzle_target; + render_gauge(widgets.nozzle, live ? nozzle : NAN, live ? celsius(nozzle, data.nozzle_unit) : NAN, + 300, live ? "/ " + number(target) + " " + data.nozzle_unit : "--"); + render_gauge(widgets.bed, live ? data.bed : NAN, live ? celsius(data.bed, data.bed_unit) : NAN, + 120, live ? "/ " + number(data.bed_target) + " " + data.bed_unit : "--"); + render_gauge(widgets.fan, live ? data.fan : NAN, live ? data.fan : NAN, 100, "%"); + if (data.dual_nozzle) + render_gauge(widgets.aux, live ? data.right_nozzle : NAN, + live ? celsius(data.right_nozzle, data.nozzle_unit) : NAN, + 300, live ? "/ " + number(data.right_target) + " " + data.nozzle_unit : "--"); + else + render_gauge(widgets.aux, live ? data.aux_fan : NAN, live ? data.aux_fan : NAN, 100, "%"); + render_gauge(widgets.exhaust, live ? data.exhaust_fan : NAN, live ? data.exhaust_fan : NAN, 100, "%"); + } std::string time_line = live ? remaining(data.time_left, data.remaining_unit) : "--"; if (live && !data.finish_time.empty()) time_line = data.finish_time + " | " + time_line; set_text(widgets.remaining, time_line); set_text(widgets.layers, live ? "Layer " + number(data.layer) + " / " + number(data.total_layers) : "--"); 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(); + if (live && (data.has_print_error() || is_on(data.hms_error))) stage = data.alert_summary(); 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()); diff --git a/esphome/bambu-display/generate.py b/esphome/bambu-display/generate.py index 05013f3..7f163c5 100644 --- a/esphome/bambu-display/generate.py +++ b/esphome/bambu-display/generate.py @@ -23,6 +23,13 @@ DUAL = { EXTRA_FANS = {'aux_fan': 'aux_fan_speed', 'exhaust_fan': 'chamber_fan_speed'} TEXT = {'status': 'print_status', 'task': 'task_name', 'stage': 'current_stage'} BINARY = {'online': 'online', 'print_error': 'print_error', 'hms_error': 'hms_errors'} +ELEGOO_COMMON = {'progress': 'percent_complete', 'time_left': 'remaining_print_time', + 'layer': 'current_layer', 'total_layers': 'total_layers'} +ELEGOO_FDM = {'nozzle': 'nozzle_temperature', 'bed': 'bed_temperature', + 'nozzle_target': ('number', 'target_nozzle_temp'), + 'bed_target': ('number', 'target_bed_temp'), 'fan': 'model_fan_speed', + 'aux_fan': 'auxiliary_fan_speed', 'exhaust_fan': 'enclosure_fan_speed'} +ELEGOO_RESIN = {'uv': 'uv_led_temp', 'elapsed': 'current_print_time'} BG, CARD, TRACK, TEXT_COLOR, MUTED, GREEN = ( 0x000000, 0x000000, 0x19232B, 0xEEEEEE, 0xAEB3B7, 0x00FF00) ORANGE, CYAN = 0xFFAA00, 0x00E5E5 @@ -65,6 +72,11 @@ def validate_printers(printers): if not re.fullmatch(r'[a-z][a-z0-9_]*', pid) or pid in ids: raise ValueError(f'Invalid or duplicate printer id: {pid}') ids.add(pid) + profile = printer.get('profile', 'bambu') + if profile not in ('bambu', 'elegoo_fdm', 'elegoo_resin'): + raise ValueError(f'{pid}: unsupported profile: {profile}') + if profile != 'bambu' and (printer.get('dual_nozzle') or trays_for(printer) or printer.get('extra_fans')): + raise ValueError(f'{pid}: dual_nozzle, filament slots and extra_fans require the bambu profile') if len(trays_for(printer)) > 8: raise ValueError(f'{pid}: the filament strip fits up to eight slots') if 'dual_nozzle' in printer and not isinstance(printer['dual_nozzle'], bool): @@ -72,7 +84,7 @@ def validate_printers(printers): if any(key not in EXTRA_FANS for key in printer.get('extra_fans', [])): raise ValueError(f'{pid}: extra_fans supports aux_fan and exhaust_fan') for value in printer.get('entities', {}).values(): - if not isinstance(value, str) or not re.fullmatch(r'(sensor|binary_sensor)\.[a-z0-9_]+', value): + if not isinstance(value, str) or not re.fullmatch(r'(sensor|binary_sensor|number)\.[a-z0-9_]+', value): raise ValueError(f'{pid}: invalid entity override: {value}') @@ -96,8 +108,22 @@ def gauge(pid, key, title, x, y, color, maximum): ] +def fdm_gauges(printer): + pid = printer['id'] + widgets = gauge(pid, 'nozzle', 'Nozzle L' if printer.get('dual_nozzle') else 'Nozzle', + 202, 62, ORANGE, 300) + widgets += gauge(pid, 'bed', 'Bed', 388, 62, CYAN, 120) + widgets += gauge(pid, 'fan', 'Part', 16, 229, 0xEEEEEE, 100) + widgets += gauge(pid, 'aux', 'Nozzle R' if printer.get('dual_nozzle') else 'Aux', + 202, 229, ORANGE if printer.get('dual_nozzle') else 0xEEEEEE, + 300 if printer.get('dual_nozzle') else 100) + widgets += gauge(pid, 'exhaust', 'Exhaust', 388, 229, GREEN, 100) + return widgets + + def page_for(printer, index, total): pid = printer['id'] + resin = printer.get('profile') == 'elegoo_resin' widgets = [ {'bar': {'id': f'{pid}_top_progress', 'x': 0, 'y': 0, 'width': 800, 'height': 5, 'min_value': 0, 'max_value': 100, 'value': 0, 'bg_color': TRACK, @@ -110,16 +136,17 @@ def page_for(printer, index, total): 'border_width': 0, 'pad_all': 0, 'scrollable': False, 'gesture_bubble': True}}, ] widgets += gauge(pid, 'progress', 'Progress', 16, 62, GREEN, 100) - widgets += gauge(pid, 'nozzle', 'Nozzle L' if printer.get('dual_nozzle') else 'Nozzle', - 202, 62, ORANGE, 300) - widgets += gauge(pid, 'bed', 'Bed', 388, 62, CYAN, 120) - widgets += gauge(pid, 'fan', 'Part', 16, 229, 0xEEEEEE, 100) - widgets += gauge(pid, 'aux', 'Nozzle R' if printer.get('dual_nozzle') else 'Aux', - 202, 229, ORANGE if printer.get('dual_nozzle') else 0xEEEEEE, - 300 if printer.get('dual_nozzle') else 100) - widgets += gauge(pid, 'exhaust', 'Exhaust', 388, 229, GREEN, 100) + if resin: + widgets += gauge(pid, 'uv', 'UV LED', 202, 62, ORANGE, 100) + widgets += gauge(pid, 'layer', 'Layer', 388, 62, CYAN, 100) + widgets += [label('FINISH / TIME REMAINING', 16, 260, 544, 18, + text_align='CENTER', text_color=MUTED), + label('--', 16, 354, 544, 20, id=f'{pid}_elapsed_label', + text_align='CENTER', text_color=MUTED)] + else: + widgets += fdm_gauges(printer) widgets += [ - label('--', 16, 401, 544, 26, id=f'{pid}_remaining_label', text_color=GREEN, text_align='CENTER'), + label('--', 16, 300 if resin else 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', long_mode='SCROLL_CIRCULAR', clickable=True, on_click=[{'lvgl.widget.show': f'{pid}_health_panel'}]), @@ -146,7 +173,10 @@ def page_for(printer, index, total): id=f'{pid}_tray_{i}_label', text_align='CENTER'), ]}}) if not trays: - widgets.append(label('No AMS', 594, 150, 190, 20, text_color=MUTED, text_align='CENTER')) + widgets.append(label('ELEGOO' if printer.get('profile', '').startswith('elegoo_') else 'No AMS', 594, 150, 190, 20, + text_color=MUTED, text_align='CENTER')) + widgets.append(label(printer['subtitle'], 594, 185, 190, 16, + text_color=MUTED, text_align='CENTER')) 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, @@ -208,30 +238,44 @@ def generate(printers): 'interval': [{'interval': '1s', 'then': []}], 'lvgl': {'pages': []}} for index, printer in enumerate(printers): pid = printer['id'] - fields = dict(NUMERIC) + profile = printer.get('profile', 'bambu') + elegoo = profile.startswith('elegoo_') + resin = profile == 'elegoo_resin' + fields = {**ELEGOO_COMMON, **(ELEGOO_RESIN if resin else ELEGOO_FDM)} if elegoo else dict(NUMERIC) if printer.get('dual_nozzle'): fields.update(DUAL) fields.update({key: EXTRA_FANS[key] for key in printer.get('extra_fans', [])}) expressions = {} for key, suffix in fields.items(): sid = f'{pid}_{key}' - eid = entity(printer, key, suffix) + domain, suffix = suffix if isinstance(suffix, tuple) else ('sensor', suffix) + eid = entity(printer, key, suffix, domain) package['sensor'].append({'platform': 'homeassistant', 'id': sid, 'entity_id': eid, 'internal': True}) expressions[key] = f'id({sid}).state' - 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) - 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')]: - text_fields[key] = (entity(printer, numeric_key, NUMERIC[numeric_key]), 'unit_of_measurement') + if elegoo: + text_fields = {key: (entity(printer, key, suffix), None) for key, suffix in { + 'status': 'print_status', 'task': 'file_name', 'stage': 'current_status', + 'print_error': 'print_error', 'print_error_reason': 'error_status_reason'}.items()} + text_fields['online'] = (entity(printer, 'online', 'sdcp_status', 'binary_sensor'), None) + else: + 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) + 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'), ('uv_unit', 'uv'), ('elapsed_unit', 'elapsed')]: + if numeric_key in fields: + suffix = fields[numeric_key] + domain, suffix = suffix if isinstance(suffix, tuple) else ('sensor', suffix) + text_fields[key] = (entity(printer, numeric_key, suffix, domain), 'unit_of_measurement') trays = trays_for(printer) for i, tray in enumerate(trays): for key, attr in [('type', 'type'), ('color', 'color'), ('active', 'active')]: @@ -248,9 +292,14 @@ def generate(printers): for key, expr in expressions.items(): if not key.startswith(('tray_', 'hms_1_', 'hms_2_', 'hms_3_')): 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 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 += [ @@ -270,8 +319,13 @@ def generate(printers): 'status_label', 'task_label', 'top_progress', 'remaining_label', 'layers_label', 'stage_label', 'health_details')] 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')) + '}' + if not resin or key == 'progress' else '{}') refs.append('{' + ', '.join(tray_widgets) + '}') + if resin: + for key in ('uv', 'layer'): + refs.append('{' + ', '.join(f'id({pid}_{key}_{part})' for part in ('arc', 'label', 'detail')) + '}') + refs.append(f'id({pid}_elapsed_label)') lines.append('bambu_display::render(data, {' + ', '.join(refs) + '});') package['script'].append({'id': f'{pid}_refresh', 'then': [{'lambda': Lambda('\n'.join(lines))}]}) package['interval'][0]['then'].append({'if': { diff --git a/esphome/bambu-display/generated.yaml b/esphome/bambu-display/generated.yaml index 74c233f..c783aec 100644 --- a/esphome/bambu-display/generated.yaml +++ b/esphome/bambu-display/generated.yaml @@ -208,6 +208,74 @@ sensor: id: constipation_orion_exhaust_fan entity_id: sensor.constipation_orion_chamber_fan_speed internal: true +- platform: homeassistant + id: centauri_carbon_progress + entity_id: sensor.centauri_carbon_percent_complete + internal: true +- platform: homeassistant + id: centauri_carbon_time_left + entity_id: sensor.centauri_carbon_remaining_print_time + internal: true +- platform: homeassistant + id: centauri_carbon_layer + entity_id: sensor.centauri_carbon_current_layer + internal: true +- platform: homeassistant + id: centauri_carbon_total_layers + entity_id: sensor.centauri_carbon_total_layers + internal: true +- platform: homeassistant + id: centauri_carbon_nozzle + entity_id: sensor.centauri_carbon_nozzle_temperature + internal: true +- platform: homeassistant + id: centauri_carbon_bed + entity_id: sensor.centauri_carbon_bed_temperature + internal: true +- platform: homeassistant + id: centauri_carbon_nozzle_target + entity_id: number.centauri_carbon_target_nozzle_temp + internal: true +- platform: homeassistant + id: centauri_carbon_bed_target + entity_id: number.centauri_carbon_target_bed_temp + internal: true +- platform: homeassistant + id: centauri_carbon_fan + entity_id: sensor.centauri_carbon_model_fan_speed + internal: true +- platform: homeassistant + id: centauri_carbon_aux_fan + entity_id: sensor.centauri_carbon_auxiliary_fan_speed + internal: true +- platform: homeassistant + id: centauri_carbon_exhaust_fan + entity_id: sensor.centauri_carbon_enclosure_fan_speed + internal: true +- platform: homeassistant + id: saturn_progress + entity_id: sensor.saturn_percent_complete + internal: true +- platform: homeassistant + id: saturn_time_left + entity_id: sensor.saturn_remaining_print_time + internal: true +- platform: homeassistant + id: saturn_layer + entity_id: sensor.saturn_current_layer + internal: true +- platform: homeassistant + id: saturn_total_layers + entity_id: sensor.saturn_total_layers + internal: true +- platform: homeassistant + id: saturn_uv + entity_id: sensor.saturn_uv_led_temp + internal: true +- platform: homeassistant + id: saturn_elapsed + entity_id: sensor.saturn_current_print_time + internal: true text_sensor: - platform: homeassistant id: prints_charming_status @@ -1079,6 +1147,84 @@ text_sensor: entity_id: sensor.h2c_31b8ap5a2200288_ams_2_tray_4 internal: true attribute: active +- platform: homeassistant + id: centauri_carbon_status + entity_id: sensor.centauri_carbon_print_status + internal: true +- platform: homeassistant + id: centauri_carbon_task + entity_id: sensor.centauri_carbon_file_name + internal: true +- platform: homeassistant + id: centauri_carbon_stage + entity_id: sensor.centauri_carbon_current_status + internal: true +- platform: homeassistant + id: centauri_carbon_print_error + entity_id: sensor.centauri_carbon_print_error + internal: true +- platform: homeassistant + id: centauri_carbon_print_error_reason + entity_id: sensor.centauri_carbon_error_status_reason + internal: true +- platform: homeassistant + id: centauri_carbon_online + entity_id: binary_sensor.centauri_carbon_sdcp_status + internal: true +- platform: homeassistant + id: centauri_carbon_remaining_unit + entity_id: sensor.centauri_carbon_remaining_print_time + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: centauri_carbon_nozzle_unit + entity_id: sensor.centauri_carbon_nozzle_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: centauri_carbon_bed_unit + entity_id: sensor.centauri_carbon_bed_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: saturn_status + entity_id: sensor.saturn_print_status + internal: true +- platform: homeassistant + id: saturn_task + entity_id: sensor.saturn_file_name + internal: true +- platform: homeassistant + id: saturn_stage + entity_id: sensor.saturn_current_status + internal: true +- platform: homeassistant + id: saturn_print_error + entity_id: sensor.saturn_print_error + internal: true +- platform: homeassistant + id: saturn_print_error_reason + entity_id: sensor.saturn_error_status_reason + internal: true +- platform: homeassistant + id: saturn_online + entity_id: binary_sensor.saturn_sdcp_status + internal: true +- platform: homeassistant + id: saturn_remaining_unit + entity_id: sensor.saturn_remaining_print_time + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: saturn_uv_unit + entity_id: sensor.saturn_uv_led_temp + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: saturn_elapsed_unit + entity_id: sensor.saturn_current_print_time + internal: true + attribute: unit_of_measurement script: - id: prints_charming_refresh then: @@ -1283,6 +1429,64 @@ script: 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::render(data, {id(constipation_orion_status_label), id(constipation_orion_task_label), id(constipation_orion_top_progress), id(constipation_orion_remaining_label), id(constipation_orion_layers_label), id(constipation_orion_stage_label), id(constipation_orion_health_details), {id(constipation_orion_progress_arc), id(constipation_orion_progress_label), id(constipation_orion_progress_detail)}, {id(constipation_orion_nozzle_arc), id(constipation_orion_nozzle_label), id(constipation_orion_nozzle_detail)}, {id(constipation_orion_bed_arc), id(constipation_orion_bed_label), id(constipation_orion_bed_detail)}, {id(constipation_orion_fan_arc), id(constipation_orion_fan_label), id(constipation_orion_fan_detail)}, {id(constipation_orion_aux_arc), id(constipation_orion_aux_label), id(constipation_orion_aux_detail)}, {id(constipation_orion_exhaust_arc), id(constipation_orion_exhaust_label), id(constipation_orion_exhaust_detail)}, {{id(constipation_orion_tray_0_card), id(constipation_orion_tray_0_swatch), id(constipation_orion_tray_0_label)}, {id(constipation_orion_tray_1_card), id(constipation_orion_tray_1_swatch), id(constipation_orion_tray_1_label)}, {id(constipation_orion_tray_2_card), id(constipation_orion_tray_2_swatch), id(constipation_orion_tray_2_label)}, {id(constipation_orion_tray_3_card), id(constipation_orion_tray_3_swatch), id(constipation_orion_tray_3_label)}, {id(constipation_orion_tray_4_card), id(constipation_orion_tray_4_swatch), id(constipation_orion_tray_4_label)}, {id(constipation_orion_tray_5_card), id(constipation_orion_tray_5_swatch), id(constipation_orion_tray_5_label)}, {id(constipation_orion_tray_6_card), id(constipation_orion_tray_6_swatch), id(constipation_orion_tray_6_label)}, {id(constipation_orion_tray_7_card), id(constipation_orion_tray_7_swatch), id(constipation_orion_tray_7_label)}}}); +- id: centauri_carbon_refresh + then: + - lambda: !lambda |- + 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::render(data, {id(centauri_carbon_status_label), id(centauri_carbon_task_label), id(centauri_carbon_top_progress), id(centauri_carbon_remaining_label), id(centauri_carbon_layers_label), id(centauri_carbon_stage_label), id(centauri_carbon_health_details), {id(centauri_carbon_progress_arc), id(centauri_carbon_progress_label), id(centauri_carbon_progress_detail)}, {id(centauri_carbon_nozzle_arc), id(centauri_carbon_nozzle_label), id(centauri_carbon_nozzle_detail)}, {id(centauri_carbon_bed_arc), id(centauri_carbon_bed_label), id(centauri_carbon_bed_detail)}, {id(centauri_carbon_fan_arc), id(centauri_carbon_fan_label), id(centauri_carbon_fan_detail)}, {id(centauri_carbon_aux_arc), id(centauri_carbon_aux_label), id(centauri_carbon_aux_detail)}, {id(centauri_carbon_exhaust_arc), id(centauri_carbon_exhaust_label), id(centauri_carbon_exhaust_detail)}, {}}); +- id: saturn_refresh + then: + - lambda: !lambda |- + 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::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)}); interval: - interval: 1s then: @@ -1311,6 +1515,16 @@ interval: lvgl.page.is_showing: constipation_orion_page then: - script.execute: constipation_orion_refresh + - if: + condition: + lvgl.page.is_showing: centauri_carbon_page + then: + - script.execute: centauri_carbon_refresh + - if: + condition: + lvgl.page.is_showing: saturn_page + then: + - script.execute: saturn_refresh lvgl: pages: - id: prints_charming_page @@ -1358,7 +1572,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 1 / 5 + text: 1 / 7 x: 520 y: 24 width: 70 @@ -2247,7 +2461,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 2 / 5 + text: 2 / 7 x: 520 y: 24 width: 70 @@ -2845,7 +3059,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 3 / 5 + text: 3 / 7 x: 520 y: 24 width: 70 @@ -3563,7 +3777,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 4 / 5 + text: 4 / 7 x: 520 y: 24 width: 70 @@ -4281,7 +4495,7 @@ lvgl: long_mode: DOT gesture_bubble: true - label: - text: 5 / 5 + text: 5 / 7 x: 520 y: 24 width: 70 @@ -5125,6 +5339,1008 @@ lvgl: align: CENTER on_click: - lvgl.widget.hide: constipation_orion_health_panel + - id: centauri_carbon_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: centauri_carbon_refresh + widgets: + - bar: + id: centauri_carbon_top_progress + x: 0 + y: 0 + width: 800 + height: 5 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - label: + text: Centauri Carbon + x: 16 + y: 17 + width: 485 + height: 34 + text_color: 65280 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + - label: + text: 6 / 7 + x: 520 + y: 24 + width: 70 + height: 22 + text_color: 11449271 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - label: + text: SYNCING + x: 598 + y: 21 + width: 186 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_status_label + text_align: RIGHT + clickable: true + on_click: + - lvgl.widget.show: centauri_carbon_health_panel + - obj: + x: 578 + y: 64 + width: 1 + height: 362 + bg_color: 1647403 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - arc: + id: centauri_carbon_progress_arc + x: 24 + y: 62 + width: 154 + height: 154 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 65280 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 30 + y: 109 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_progress_label + text_align: CENTER + - label: + text: -- + x: 30 + y: 148 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_progress_detail + text_align: CENTER + - label: + text: Progress + x: 16 + y: 199 + width: 170 + height: 26 + text_color: 65280 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - arc: + id: centauri_carbon_nozzle_arc + x: 210 + y: 62 + width: 154 + height: 154 + min_value: 0 + max_value: 300 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 16755200 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 216 + y: 109 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_nozzle_label + text_align: CENTER + - label: + text: -- + x: 216 + y: 148 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_nozzle_detail + text_align: CENTER + - label: + text: Nozzle + x: 202 + y: 199 + width: 170 + height: 26 + text_color: 16755200 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - arc: + id: centauri_carbon_bed_arc + x: 396 + y: 62 + width: 154 + height: 154 + min_value: 0 + max_value: 120 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 58853 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 402 + y: 109 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_bed_label + text_align: CENTER + - label: + text: -- + x: 402 + y: 148 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_bed_detail + text_align: CENTER + - label: + text: Bed + x: 388 + y: 199 + width: 170 + height: 26 + text_color: 58853 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - arc: + id: centauri_carbon_fan_arc + x: 24 + y: 229 + width: 154 + height: 154 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 15658734 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 30 + y: 276 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_fan_label + text_align: CENTER + - label: + text: -- + x: 30 + y: 315 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_fan_detail + text_align: CENTER + - label: + text: Part + x: 16 + y: 366 + width: 170 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - arc: + id: centauri_carbon_aux_arc + x: 210 + y: 229 + width: 154 + height: 154 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 15658734 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 216 + y: 276 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_aux_label + text_align: CENTER + - label: + text: -- + x: 216 + y: 315 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_aux_detail + text_align: CENTER + - label: + text: Aux + x: 202 + y: 366 + width: 170 + height: 26 + text_color: 15658734 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - arc: + id: centauri_carbon_exhaust_arc + x: 396 + y: 229 + width: 154 + height: 154 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 65280 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 402 + y: 276 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_exhaust_label + text_align: CENTER + - label: + text: -- + x: 402 + y: 315 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_exhaust_detail + text_align: CENTER + - label: + text: Exhaust + x: 388 + y: 366 + width: 170 + height: 26 + text_color: 65280 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - label: + text: -- + x: 16 + y: 401 + width: 544 + height: 34 + text_color: 65280 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_remaining_label + text_align: CENTER + - label: + text: Telemetry unavailable + x: 16 + y: 430 + width: 544 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: SCROLL_CIRCULAR + gesture_bubble: true + id: centauri_carbon_stage_label + text_align: CENTER + clickable: true + on_click: + - lvgl.widget.show: centauri_carbon_health_panel + - label: + text: -- + x: 594 + y: 397 + width: 190 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_layers_label + text_align: CENTER + - label: + text: ELEGOO + x: 594 + y: 150 + width: 190 + height: 28 + text_color: 11449271 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - label: + text: FDM / Office + x: 594 + y: 185 + width: 190 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - 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: Waiting for Home Assistant + x: 94 + y: 449 + width: 612 + height: 22 + text_color: 15658734 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + id: centauri_carbon_task_label + text_align: CENTER + - obj: + id: centauri_carbon_health_panel + x: 24 + y: 62 + width: 752 + height: 366 + hidden: true + bg_color: 0 + bg_opa: COVER + border_width: 2 + border_color: 11449271 + radius: 10 + pad_all: 0 + scrollable: false + gesture_bubble: false + widgets: + - label: + text: Printer health / Centauri Carbon + x: 16 + y: 14 + width: 716 + height: 28 + text_color: 15658734 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + - obj: + x: 12 + y: 50 + width: 724 + height: 246 + bg_color: 0 + border_width: 0 + pad_all: 0 + scroll_dir: VER + scrollbar_mode: AUTO + gesture_bubble: false + widgets: + - label: + text: No current health notifications. + x: 0 + y: 0 + width: 700 + height: SIZE_CONTENT + text_color: 15658734 + text_font: montserrat_18 + long_mode: WRAP + gesture_bubble: true + id: centauri_carbon_health_details + - button: + x: 624 + y: 310 + width: 108 + height: 40 + bg_color: 1647403 + radius: 6 + widgets: + - label: + text: CLOSE + align: CENTER + on_click: + - lvgl.widget.hide: centauri_carbon_health_panel + - id: saturn_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: saturn_refresh + widgets: + - bar: + id: saturn_top_progress + x: 0 + y: 0 + width: 800 + height: 5 + min_value: 0 + max_value: 100 + value: 0 + bg_color: 1647403 + indicator: + bg_color: 65280 + gesture_bubble: true + - label: + text: Saturn + x: 16 + y: 17 + width: 485 + height: 34 + text_color: 65280 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + - label: + text: 7 / 7 + x: 520 + y: 24 + width: 70 + height: 22 + text_color: 11449271 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - label: + text: SYNCING + x: 598 + y: 21 + width: 186 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: saturn_status_label + text_align: RIGHT + clickable: true + on_click: + - lvgl.widget.show: saturn_health_panel + - obj: + x: 578 + y: 64 + width: 1 + height: 362 + bg_color: 1647403 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - arc: + id: saturn_progress_arc + x: 24 + y: 62 + width: 154 + height: 154 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 65280 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 30 + y: 109 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: saturn_progress_label + text_align: CENTER + - label: + text: -- + x: 30 + y: 148 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: saturn_progress_detail + text_align: CENTER + - label: + text: Progress + x: 16 + y: 199 + width: 170 + height: 26 + text_color: 65280 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - arc: + id: saturn_uv_arc + x: 210 + y: 62 + width: 154 + height: 154 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 16755200 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 216 + y: 109 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: saturn_uv_label + text_align: CENTER + - label: + text: -- + x: 216 + y: 148 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: saturn_uv_detail + text_align: CENTER + - label: + text: UV LED + x: 202 + y: 199 + width: 170 + height: 26 + text_color: 16755200 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - arc: + id: saturn_layer_arc + x: 396 + y: 62 + width: 154 + height: 154 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 13 + arc_color: 1647403 + indicator: + arc_color: 58853 + arc_width: 13 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 402 + y: 109 + width: 142 + height: 40 + text_color: 15658734 + text_font: montserrat_32 + long_mode: DOT + gesture_bubble: true + id: saturn_layer_label + text_align: CENTER + - label: + text: -- + x: 402 + y: 148 + width: 142 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: saturn_layer_detail + text_align: CENTER + - label: + text: Layer + x: 388 + y: 199 + width: 170 + height: 26 + text_color: 58853 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - label: + text: FINISH / TIME REMAINING + x: 16 + y: 260 + width: 544 + height: 26 + text_color: 11449271 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - label: + text: -- + x: 16 + y: 354 + width: 544 + height: 28 + text_color: 11449271 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + id: saturn_elapsed_label + text_align: CENTER + - label: + text: -- + x: 16 + y: 300 + width: 544 + height: 34 + text_color: 65280 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: saturn_remaining_label + text_align: CENTER + - label: + text: Telemetry unavailable + x: 16 + y: 430 + width: 544 + height: 20 + text_color: 11449271 + text_font: montserrat_12 + long_mode: SCROLL_CIRCULAR + gesture_bubble: true + id: saturn_stage_label + text_align: CENTER + clickable: true + on_click: + - lvgl.widget.show: saturn_health_panel + - label: + text: -- + x: 594 + y: 397 + width: 190 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: saturn_layers_label + text_align: CENTER + - label: + text: ELEGOO + x: 594 + y: 150 + width: 190 + height: 28 + text_color: 11449271 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - label: + text: Resin / Garage + x: 594 + y: 185 + width: 190 + height: 24 + text_color: 11449271 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - 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: Waiting for Home Assistant + x: 94 + y: 449 + width: 612 + height: 22 + text_color: 15658734 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + id: saturn_task_label + text_align: CENTER + - obj: + id: saturn_health_panel + x: 24 + y: 62 + width: 752 + height: 366 + hidden: true + bg_color: 0 + bg_opa: COVER + border_width: 2 + border_color: 11449271 + radius: 10 + pad_all: 0 + scrollable: false + gesture_bubble: false + widgets: + - label: + text: Printer health / Saturn + x: 16 + y: 14 + width: 716 + height: 28 + text_color: 15658734 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + - obj: + x: 12 + y: 50 + width: 724 + height: 246 + bg_color: 0 + border_width: 0 + pad_all: 0 + scroll_dir: VER + scrollbar_mode: AUTO + gesture_bubble: false + widgets: + - label: + text: No current health notifications. + x: 0 + y: 0 + width: 700 + height: SIZE_CONTENT + text_color: 15658734 + text_font: montserrat_18 + long_mode: WRAP + gesture_bubble: true + id: saturn_health_details + - button: + x: 624 + y: 310 + width: 108 + height: 40 + bg_color: 1647403 + radius: 6 + widgets: + - label: + text: CLOSE + align: CENTER + on_click: + - lvgl.widget.hide: saturn_health_panel - id: color_test_page skip: true bg_color: 0 diff --git a/esphome/bambu-display/preview.py b/esphome/bambu-display/preview.py index 01b32e0..3579c3b 100644 --- a/esphome/bambu-display/preview.py +++ b/esphome/bambu-display/preview.py @@ -22,11 +22,12 @@ def main(): demo_values = {'progress': 67, 'nozzle': 218, 'nozzle_target': 220, 'bed': 60, 'bed_target': 60, 'time_left': 1.75, 'layer': 134, 'total_layers': 200, 'fan': 80, 'aux_fan': 45, 'exhaust_fan': 30, 'left_nozzle': 219, 'left_target': 220, - 'right_nozzle': 37, 'right_target': 0} + 'right_nozzle': 37, 'right_target': 0, 'uv': 42, 'elapsed': 30} demo_text = {'status': 'running', 'task': 'Workshop organizer - plate 2', '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', + 'uv_unit': '°C', 'elapsed_unit': 'min', 'print_error_reason': 'ok'} colors = ['#60DE9AFF', '#F4C96BFF', '#74A9E8FF', '#F8F4ECFF', '#DF8BBFFF', '#FA936BFF', '#8E79CAFF', '#4CC9C1FF'] for printer in printers: @@ -38,6 +39,8 @@ def main(): key = entry['id'][len(prefix):] if section == 'sensor': value = str(demo_values[key]) + if key == 'time_left' and printer.get('profile', '').startswith('elegoo_'): + value = '105' else: if key.startswith('tray_'): _, index, attribute = key.split('_') @@ -47,6 +50,9 @@ def main(): text = '' else: text = demo_text[key] + if printer.get('profile', '').startswith('elegoo_'): + text = {'status': 'printing', 'print_error': 'none', + 'remaining_unit': 'min'}.get(key, text) value = 'std::string(' + json.dumps(text, ensure_ascii=False) + ')' entry.clear() entry.update({'platform': 'template', 'id': prefix + key, 'internal': True, @@ -86,6 +92,25 @@ inline void setup() { steps += [{'lvgl.page.show': printer['id'] + '_page'}, {'script.execute': printer['id'] + '_refresh'}, {'delay': '300ms'}, {'snapshot.take': {'id': 'panel_display', 'filename': printer['id'] + '.bmp'}}] + if printer.get('profile', '').startswith('elegoo_'): + pid = printer['id'] + checks = {f'{pid}_status_label': 'PRINTING', f'{pid}_progress_detail': '1h 45m'} + if printer['profile'] == 'elegoo_resin': + checks.update({f'{pid}_uv_label': '42', f'{pid}_layer_label': '134', + f'{pid}_elapsed_label': 'Elapsed: 0h 30m'}) + for widget, expected in checks.items(): + steps.append({'lambda': Lambda(f'if (std::string(lv_label_get_text(id({widget}))) != {json.dumps(expected)}) {{ ESP_LOGE("preview", "Elegoo telemetry render failed: {widget}"); exit(4); }}')}) + steps += [ + {'text_sensor.template.publish': {'id': f'{pid}_print_error_reason', 'state': 'filament_runout'}}, + {'script.execute': f'{pid}_refresh'}, + {'lambda': Lambda(f'if (std::string(lv_label_get_text(id({pid}_status_label))) != "PRINT ERROR" || std::string(lv_label_get_text(id({pid}_health_details))).find("Filament runout") == std::string::npos) {{ ESP_LOGE("preview", "Elegoo error render failed"); exit(5); }}')}, + {'text_sensor.template.publish': {'id': f'{pid}_online', 'state': 'off'}}, + {'script.execute': f'{pid}_refresh'}, + {'lambda': Lambda(f'if (std::string(lv_label_get_text(id({pid}_status_label))) != "PRINTER OFFLINE" || std::string(lv_label_get_text(id({pid}_progress_label))) != "--") {{ ESP_LOGE("preview", "Elegoo offline render failed"); exit(6); }}')}, + {'text_sensor.template.publish': {'id': f'{pid}_print_error_reason', 'state': 'ok'}}, + {'text_sensor.template.publish': {'id': f'{pid}_online', 'state': 'on'}}, + {'script.execute': f'{pid}_refresh'}, + ] steps += [{'lvgl.page.show': 'color_test_page'}, {'delay': '300ms'}, {'snapshot.take': {'id': 'panel_display', 'filename': 'color_test.bmp'}}] demo_printer = printers[0]['id'] @@ -147,6 +172,7 @@ inline void setup() { print(f'Saved {len(printers)} demo screenshots to {args.output}') print('Passed LVGL pointer swipe tests, including wraparound and button gesture consumption.') print('Passed status-tap health panel open and Close-button tests.') + print('Passed Elegoo telemetry, text-error and offline rendering checks.') if __name__ == '__main__': diff --git a/esphome/bambu-display/previews/centauri_carbon.png b/esphome/bambu-display/previews/centauri_carbon.png new file mode 100644 index 0000000..7bed42d Binary files /dev/null and b/esphome/bambu-display/previews/centauri_carbon.png differ diff --git a/esphome/bambu-display/previews/constipation_orion.png b/esphome/bambu-display/previews/constipation_orion.png index af5d046..ddc2513 100644 Binary files a/esphome/bambu-display/previews/constipation_orion.png and b/esphome/bambu-display/previews/constipation_orion.png differ diff --git a/esphome/bambu-display/previews/ds_turd_flinger.png b/esphome/bambu-display/previews/ds_turd_flinger.png index 60de212..19ce5e8 100644 Binary files a/esphome/bambu-display/previews/ds_turd_flinger.png and b/esphome/bambu-display/previews/ds_turd_flinger.png differ diff --git a/esphome/bambu-display/previews/health_notice.png b/esphome/bambu-display/previews/health_notice.png index bca6f54..daecab1 100644 Binary files a/esphome/bambu-display/previews/health_notice.png and b/esphome/bambu-display/previews/health_notice.png differ diff --git a/esphome/bambu-display/previews/jordyns_layer_slayer.png b/esphome/bambu-display/previews/jordyns_layer_slayer.png index b73c4d8..c0189c9 100644 Binary files a/esphome/bambu-display/previews/jordyns_layer_slayer.png and b/esphome/bambu-display/previews/jordyns_layer_slayer.png differ diff --git a/esphome/bambu-display/previews/polly_pocket.png b/esphome/bambu-display/previews/polly_pocket.png index b586bbf..201460d 100644 Binary files a/esphome/bambu-display/previews/polly_pocket.png and b/esphome/bambu-display/previews/polly_pocket.png differ diff --git a/esphome/bambu-display/previews/prints_charming.png b/esphome/bambu-display/previews/prints_charming.png index a73c508..808e728 100644 Binary files a/esphome/bambu-display/previews/prints_charming.png and b/esphome/bambu-display/previews/prints_charming.png differ diff --git a/esphome/bambu-display/previews/saturn.png b/esphome/bambu-display/previews/saturn.png new file mode 100644 index 0000000..f4f6dab Binary files /dev/null and b/esphome/bambu-display/previews/saturn.png differ diff --git a/esphome/bambu-display/printers.yaml b/esphome/bambu-display/printers.yaml index 4c59b56..8b9eb63 100644 --- a/esphome/bambu-display/printers.yaml +++ b/esphome/bambu-display/printers.yaml @@ -49,3 +49,15 @@ printers: - label: B # This unit retains its original hardware-based HA entity names. entity_prefix: h2c_31b8ap5a2200288_ams_2 + + - id: centauri_carbon + name: Centauri Carbon + subtitle: FDM / Office + entity_prefix: centauri_carbon + profile: elegoo_fdm + + - id: saturn + name: Saturn + subtitle: Resin / Garage + entity_prefix: saturn + profile: elegoo_resin diff --git a/esphome/bambu-display/test_dashboard.cpp b/esphome/bambu-display/test_dashboard.cpp index 37c0417..0147d12 100644 --- a/esphome/bambu-display/test_dashboard.cpp +++ b/esphome/bambu-display/test_dashboard.cpp @@ -6,6 +6,7 @@ int main() { assert(remaining(1.75f, "h") == "1h 45m"); assert(remaining(105.0f, "min") == "1h 45m"); assert(remaining(6300.0f, "s") == "1h 45m"); + assert(remaining(6300000.0f, "ms") == "1h 45m"); assert(remaining(0.15f, "h") == "0h 09m"); assert(remaining(NAN, "h") == "--"); assert(remaining(-1, "min") == "--"); @@ -22,6 +23,7 @@ int main() { assert(gauge_value(-10, 100) == 0); assert(duration_seconds(1.75f, "h") == 6300); assert(duration_seconds(105, "min") == 6300); + assert(duration_seconds(6300000, "ms") == 6300); assert(duration_seconds(NAN, "h") == -1); assert(duration_seconds(1, "") == -1); assert(std::abs(celsius(392, "°F") - 200) < 0.01f); @@ -72,4 +74,33 @@ int main() { data.status = "finish"; assert(data.status_text() == "COMPLETE"); assert(data.status_color() == GREEN); + + // Elegoo exposes error enums rather than Bambu binary errors/HMS notices. + data.elegoo = true; + data.print_error = "none"; + data.print_error_reason = "ok"; + data.status = "printing"; + assert(data.live()); + assert(!data.has_print_error()); + assert(data.status_text() == "PRINTING"); + data.print_error_reason = "filament_runout"; + assert(data.status_text() == "PRINT ERROR"); + assert(data.status_color() == RED); + assert(data.health_details().find("Filament runout") != std::string::npos); + data.print_error = "invalid_resolution"; + assert(data.alert_summary() == "Invalid resolution / Filament runout"); + data.print_error_reason = "unavailable"; + assert(data.alert_summary() == "Invalid resolution"); + data.print_error = "unknown"; + assert(!data.has_print_error()); + data.status = "error"; + assert(data.has_print_error()); + data.status = "pausing"; + assert(data.status_color() == AMBER); + data.status = "complete"; + assert(data.status_text() == "COMPLETE"); + data.online = "off"; + assert(!data.live()); + assert(data.status_text() == "PRINTER OFFLINE"); + assert(data.health_details().find("unavailable") != std::string::npos); } diff --git a/esphome/bambu-display/test_generate.py b/esphome/bambu-display/test_generate.py index c88ed25..fc3c900 100644 --- a/esphome/bambu-display/test_generate.py +++ b/esphome/bambu-display/test_generate.py @@ -20,20 +20,57 @@ class FleetTests(unittest.TestCase): for entry in package[domain]} self.assertFalse(subscribed - known) - def test_seven_printers_have_independent_pages_and_metrics(self): - for index in (6, 7): + def test_fleet_can_expand_beyond_configured_printers(self): + expected_count = len(self.printers) + 2 + for index in (8, 9): extra = copy.deepcopy(self.printers[0]) 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')]), 7) - self.assertEqual(len(package['script']), 7) + self.assertEqual(len([page for page in package['lvgl']['pages'] if not page.get('skip')]), expected_count) + self.assertEqual(len(package['script']), expected_count) subscriptions = {entry['id']: entry['entity_id'] for entry in package['sensor']} - self.assertEqual(subscriptions['printer_7_progress'], 'sensor.new_printer_7_print_progress') + self.assertEqual(subscriptions['printer_9_progress'], 'sensor.new_printer_9_print_progress') for page in package['lvgl']['pages']: self.assertIn('lvgl.page.next', page['on_swipe_left'][-1]) self.assertIn('lvgl.page.previous', page['on_swipe_right'][-1]) + def test_elegoo_profiles_use_available_metrics_and_domains(self): + package = generate(self.printers) + subscriptions = {entry['id']: entry for domain in ('sensor', 'text_sensor') + for entry in package[domain]} + self.assertEqual(subscriptions['centauri_carbon_nozzle_target']['entity_id'], + 'number.centauri_carbon_target_nozzle_temp') + self.assertEqual(subscriptions['centauri_carbon_fan']['entity_id'], + 'sensor.centauri_carbon_model_fan_speed') + for pid in ('centauri_carbon', 'saturn'): + self.assertEqual(subscriptions[f'{pid}_online']['entity_id'], f'binary_sensor.{pid}_sdcp_status') + self.assertEqual(subscriptions[f'{pid}_remaining_unit']['entity_id'], f'sensor.{pid}_remaining_print_time') + self.assertEqual(subscriptions[f'{pid}_print_error']['entity_id'], f'sensor.{pid}_print_error') + self.assertEqual(subscriptions[f'{pid}_print_error_reason']['entity_id'], f'sensor.{pid}_error_status_reason') + self.assertNotIn(f'{pid}_hms_error', subscriptions) + self.assertNotIn('saturn_nozzle', subscriptions) + self.assertNotIn('saturn_fan', subscriptions) + self.assertEqual(subscriptions['saturn_uv_unit']['entity_id'], 'sensor.saturn_uv_led_temp') + self.assertEqual(subscriptions['saturn_elapsed_unit']['entity_id'], 'sensor.saturn_current_print_time') + + def test_resin_page_contains_only_resin_gauges(self): + package = generate(self.printers) + page = next(page for page in package['lvgl']['pages'] if page['id'] == 'saturn_page') + arcs = {widget['arc']['id'] for widget in page['widgets'] if 'arc' in widget} + self.assertEqual(arcs, {'saturn_progress_arc', 'saturn_uv_arc', 'saturn_layer_arc'}) + + def test_number_overrides_are_supported(self): + self.printers[-2]['entities'] = {'nozzle_target': 'number.custom_target'} + package = generate(self.printers) + entry = next(row for row in package['sensor'] if row['id'] == 'centauri_carbon_nozzle_target') + self.assertEqual(entry['entity_id'], 'number.custom_target') + + def test_reject_unknown_profiles(self): + self.printers[0]['profile'] = 'unsupported' + with self.assertRaisesRegex(ValueError, 'unsupported profile'): + generate(self.printers) + def test_overrides_apply_to_values_and_unit_subscriptions(self): self.printers[0]['entities'] = {'time_left': 'sensor.custom_remaining'} package = generate(self.printers)