diff --git a/.gitignore b/.gitignore index 2350c66..ae4fb16 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ ### ESPHome Folders ### !esphome/ !esphome/fonts/** +!esphome/bambu-display/ +!esphome/bambu-display/** +esphome/bambu-display/**/__pycache__/ !esphome/ha-remote/ !esphome/ha-remote/components/ !esphome/ha-remote/components/** diff --git a/esphome/bambu-display/README.md b/esphome/bambu-display/README.md new file mode 100644 index 0000000..7038926 --- /dev/null +++ b/esphome/bambu-display/README.md @@ -0,0 +1,127 @@ +# Bambu 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 +and LCD timings follow the [ESPHome device reference](https://devices.esphome.io/devices/sunton-esp32-8048s043c/). +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 +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. + +## Pages and navigation + +![LVGL preview with synthetic printer telemetry](previews/prints_charming.png) + +The configured swipe order is: + +1. PrintsCharming — P1S, Office, both four-slot AMS units. +2. Polly-Pocket — A1 mini, Office, external spool. +3. Ds-Turd-Flinger — A1, Upstairs, four-slot AMS. +4. Jordyns-Layer-Slayer — A1, Upstairs, four-slot AMS. +5. Constipation-Orion — Office, left/right nozzles and four-slot AMS. + +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 metric cards 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 +temperatures, remaining time, current/total layers, print stage, part fan speed and +filament colors/types. The active filament slot has a green outline. Print errors +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. + +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` +unit, and temperature labels use the imported HA temperature unit. Only the visible +page is refreshed each second; every printer's subscriptions remain connected. + +This version provides monitoring and local page navigation. Camera feeds, cover +images, printer pause/stop controls, power switching, cloud login and automatic +printer rotation are outside its current scope. + +## Install + +1. Keep `esphome/bambu-printer-display.yaml` and the entire `bambu-display/` folder + together in the ESPHome configuration directory. The generated package is checked + in, so the ESPHome dashboard can compile it without running the generator first. +2. Supply the existing `api_encryption_key`, `wifi_iot_ssid` and `wifi_password` + entries in that directory's `secrets.yaml`. +3. Use ESPHome **2026.9.0 or newer**. Compile and install + `bambu-printer-display.yaml` over the board's USB-to-UART bridge for the first + flash; subsequent installs can use encrypted OTA. +4. Add **Bambu Printer Display** through Home Assistant's ESPHome integration using + the same API encryption key. The display only subscribes to entities, so it does + not require permission to perform Home Assistant actions. +5. Confirm all pages receive data and verify touch direction and color order on the + physical panel. Entity IDs match the repository's `ha-entities.txt` snapshot; + renamed or disabled entities in live HA must be corrected/enabled there. + +The USB D+/D− GPIO19/20 pins are used by the board's touch bus. Logging therefore +uses UART0. ESPHome may still warn about these two pins; they are intentional for +this board. If GT911 is not found at `0x5D`, inspect the I²C scan and use its reported +address (some hardware revisions select `0x14`). Backlight is GPIO2; RGB data pins +and timings are explicit in the main YAML rather than borrowed from a 5″ board. + +CLI checks from the repository root: + +```sh +esphome config esphome/bambu-printer-display.yaml +esphome compile esphome/bambu-printer-display.yaml +``` + +## Change or expand the fleet + +Edit **`printers.yaml`**, then regenerate and flash the display: + +```sh +python esphome/bambu-display/generate.py +python esphome/bambu-display/generate.py --check +``` + +The generator requires PyYAML (already included in ESPHome's Python environment). +Do not hand-edit `generated.yaml`. There is no two-printer limit in the generator; +adding a printer creates its own subscriptions, widgets and swipe page. The practical +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. + +```yaml +- id: another_printer + name: Another Printer + subtitle: Workshop + entity_prefix: my_bambu_printer + entities: + progress: sensor.renamed_progress + time_left: sensor.renamed_remaining_time + filament_banks: + - label: A + entity_prefix: my_bambu_printer_ams_1 +``` + +For a dual-nozzle printer add `dual_nozzle: true`. For external filament add +`external_spools: [{label: EXT, entity: sensor.my_external_spool}]`. Per-metric +`entities` overrides use the keys in `NUMERIC`, `DUAL`, `TEXT` and `BINARY` inside +`generate.py`. A remaining-time override also updates its unit subscription. +Filament entries use the Bambu integration's `type`, `color` and `active` attributes. + +## Development checks and screenshots + +```sh +python -m unittest discover -s esphome/bambu-display -v +clang++ -std=c++17 -Wall -Wextra -Werror esphome/bambu-display/test_dashboard.cpp -o /tmp/bambu-dashboard-test +/tmp/bambu-dashboard-test +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 into `previews/`. 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. diff --git a/esphome/bambu-display/dashboard.h b/esphome/bambu-display/dashboard.h new file mode 100644 index 0000000..945bac2 --- /dev/null +++ b/esphome/bambu-display/dashboard.h @@ -0,0 +1,172 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace bambu_display { + +constexpr uint32_t GREEN = 0x60DE9A; +constexpr uint32_t MUTED = 0x91A3B5; +constexpr uint32_t RED = 0xFF737B; +constexpr uint32_t AMBER = 0xF2C572; +constexpr uint32_t TRACK = 0x263442; + +inline std::string lower(std::string text) { + for (char &c : text) c = static_cast(std::tolower(static_cast(c))); + return text; +} + +inline bool valid_text(const std::string &text) { + const auto value = lower(text); + return !value.empty() && value != "unknown" && value != "unavailable" && value != "none"; +} + +inline bool is_on(const std::string &text) { + const auto value = lower(text); + return value == "on" || value == "true" || value == "1"; +} + +inline std::string humanize(std::string text) { + if (!valid_text(text)) return "--"; + std::replace(text.begin(), text.end(), '_', ' '); + if (!text.empty()) text[0] = static_cast(std::toupper(static_cast(text[0]))); + return text; +} + +inline int gauge_value(float value, int maximum) { + return std::isfinite(value) ? static_cast(std::clamp(value, 0.0f, float(maximum))) : 0; +} + +inline std::string number(float value) { + if (!std::isfinite(value) || value < 0 || value > 1000000) return "--"; + char buffer[24]; + snprintf(buffer, sizeof(buffer), "%.0f", value); + return buffer; +} + +inline std::string temperature(float current, float target, const std::string &unit) { + return number(current) + " / " + number(target) + (valid_text(unit) ? " " + unit : ""); +} + +inline std::string remaining(float value, const std::string &unit) { + if (!std::isfinite(value) || value < 0) return "--"; + double minutes; + if (unit == "h") minutes = double(value) * 60; + else if (unit == "min") minutes = value; + else if (unit == "s") minutes = double(value) / 60; + else return "--"; // Never silently interpret hours as minutes. + if (minutes > 1000000) return "--"; + const auto rounded = static_cast(std::ceil(minutes - 0.001)); + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%dh %02dm", rounded / 60, rounded % 60); + return buffer; +} + +inline uint32_t filament_color(std::string value) { + if (!value.empty() && value[0] == '#') value.erase(0, 1); + // Bambu publishes RGB or RGBA, with alpha at the end. + if (value.size() != 6 && value.size() != 8) return TRACK; + uint32_t rgb = 0; + for (size_t i = 0; i < value.size(); ++i) { + const char c = static_cast(std::tolower(static_cast(value[i]))); + const int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; + if (digit < 0) return TRACK; + if (i < 6) rgb = (rgb << 4) | digit; + } + return rgb; +} + +struct Tray { + std::string label, type, color, active; +}; + +struct Snapshot { + bool connected = false; + std::string online, status, task, stage, print_error, hms_error; + std::string remaining_unit, nozzle_unit, bed_unit; + float progress = NAN, nozzle = NAN, nozzle_target = NAN, bed = NAN, bed_target = NAN; + float time_left = NAN, layer = NAN, total_layers = NAN, fan = NAN; + bool dual_nozzle = false; + float left_nozzle = NAN, left_target = NAN, right_nozzle = NAN, right_target = NAN; + std::vector trays; + + bool live() const { return connected && is_on(online) && valid_text(status); } + std::string status_text() const { + 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 (is_on(hms_error)) return "HMS ALERT"; + const auto state = lower(status); + if (state == "running") return "PRINTING"; + if (state == "pause" || state == "paused") return "PAUSED"; + if (state == "finish" || state == "finished") return "COMPLETE"; + auto text = humanize(status); + for (char &c : text) c = static_cast(std::toupper(static_cast(c))); + return text; + } + uint32_t status_color() const { + if (!live()) return MUTED; + const auto state = lower(status); + if (is_on(print_error) || is_on(hms_error) || state == "failed") return RED; + if (state == "pause" || state == "paused") return AMBER; + return GREEN; + } +}; + +} // namespace bambu_display + +#ifdef USE_LVGL +#include + +namespace bambu_display { + +struct TrayWidgets { lv_obj_t *card, *swatch, *label; }; +struct Widgets { + lv_obj_t *status, *task, *arc, *progress, *nozzle, *bed, *remaining, *layers, *stage; + std::vector trays; +}; + +inline void set_text(lv_obj_t *label, const std::string &text) { + if (text != lv_label_get_text(label)) lv_label_set_text(label, text.c_str()); +} + +inline void render(const Snapshot &data, const Widgets &widgets) { + const bool live = data.live(); + const uint32_t color = data.status_color(); + set_text(widgets.status, data.status_text()); + lv_obj_set_style_text_color(widgets.status, lv_color_hex(color), 0); + lv_obj_set_style_arc_color(widgets.arc, lv_color_hex(color), LV_PART_INDICATOR); + lv_arc_set_value(widgets.arc, live ? gauge_value(data.progress, 100) : 0); + set_text(widgets.progress, live && std::isfinite(data.progress) + ? number(std::clamp(data.progress, 0.0f, 100.0f)) + "%" : "--"); + set_text(widgets.task, live ? (valid_text(data.task) ? data.task : "No active print") + : data.connected ? "Waiting for printer data" : "Waiting for Home Assistant"); + if (live && data.dual_nozzle) { + set_text(widgets.nozzle, "L " + temperature(data.left_nozzle, data.left_target, data.nozzle_unit) + + "\nR " + temperature(data.right_nozzle, data.right_target, data.nozzle_unit)); + } else { + set_text(widgets.nozzle, live ? temperature(data.nozzle, data.nozzle_target, data.nozzle_unit) : "--"); + } + set_text(widgets.bed, live ? temperature(data.bed, data.bed_target, data.bed_unit) : "--"); + set_text(widgets.remaining, live ? remaining(data.time_left, data.remaining_unit) : "--"); + set_text(widgets.layers, live ? number(data.layer) + " / " + number(data.total_layers) : "--"); + std::string stage = live ? humanize(data.stage) : "Telemetry unavailable"; + if (live && std::isfinite(data.fan)) stage += " | Part fan " + number(data.fan) + "%"; + set_text(widgets.stage, stage); + for (size_t i = 0; i < std::min(data.trays.size(), widgets.trays.size()); ++i) { + const auto &tray = data.trays[i]; + const auto &widget = widgets.trays[i]; + set_text(widget.label, tray.label + " " + (live && valid_text(tray.type) ? tray.type : "--")); + lv_obj_set_style_bg_color(widget.swatch, lv_color_hex(live ? filament_color(tray.color) : TRACK), 0); + lv_obj_set_style_border_color(widget.card, lv_color_hex(live && is_on(tray.active) ? GREEN : TRACK), 0); + } +} + +} // namespace bambu_display +#endif diff --git a/esphome/bambu-display/generate.py b/esphome/bambu-display/generate.py new file mode 100644 index 0000000..d731287 --- /dev/null +++ b/esphome/bambu-display/generate.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""Generate the ESPHome fleet package from printers.yaml. Requires PyYAML.""" + +import argparse +import json +from pathlib import Path +import re + +import yaml + +HERE = Path(__file__).resolve().parent +NUMERIC = { + 'progress': 'print_progress', 'nozzle': 'nozzle_temperature', + 'nozzle_target': 'nozzle_target_temperature', 'bed': 'bed_temperature', + 'bed_target': 'bed_target_temperature', 'time_left': 'remaining_time', + 'layer': 'current_layer', 'total_layers': 'total_layer_count', + 'fan': 'cooling_fan_speed', +} +DUAL = { + 'left_nozzle': 'left_nozzle_temperature', 'left_target': 'left_nozzle_target_temperature', + 'right_nozzle': 'right_nozzle_temperature', 'right_target': 'right_nozzle_target_temperature', +} +TEXT = {'status': 'print_status', 'task': 'task_name', 'stage': 'current_stage'} +BINARY = {'online': 'online', 'print_error': 'print_error', 'hms_error': 'hms_errors'} +BG, CARD, TRACK, TEXT_COLOR, MUTED, GREEN = ( + 0x0D131B, 0x17222E, 0x263442, 0xEDF3F7, 0x91A3B5, 0x60DE9A) + + +class Lambda(str): + """A C++ lambda serialized with ESPHome's YAML tag.""" + + +class Dumper(yaml.SafeDumper): + def ignore_aliases(self, data): + return True + + +Dumper.add_representer(Lambda, lambda dumper, text: dumper.represent_scalar('!lambda', text, style='|')) + + +def entity(printer, key, suffix, domain='sensor'): + return printer.get('entities', {}).get(key, f'{domain}.{printer["entity_prefix"]}_{suffix}') + + +def trays_for(printer): + result = [] + for bank in printer.get('filament_banks', []): + for slot in range(1, 5): + result.append({'label': f'{bank["label"]}{slot}', + 'entity': f'sensor.{bank["entity_prefix"]}_tray_{slot}'}) + return result + printer.get('external_spools', []) + + +def validate_printers(printers): + if not isinstance(printers, list) or not printers: + raise ValueError('printers must be a nonempty list') + ids = set() + for printer in printers: + for key in ('id', 'name', 'subtitle', 'entity_prefix'): + if not isinstance(printer.get(key), str) or not printer[key]: + raise ValueError(f'Each printer needs a nonempty {key}') + pid = printer['id'] + 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) + 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): + raise ValueError(f'{pid}: dual_nozzle must be true or false') + for value in printer.get('entities', {}).values(): + if not isinstance(value, str) or not re.fullmatch(r'(sensor|binary_sensor)\.[a-z0-9_]+', value): + raise ValueError(f'{pid}: invalid entity override: {value}') + + +def label(text, x, y, width, font=18, **kwargs): + return {'label': {'text': text, 'x': x, 'y': y, 'width': width, + 'height': font + 8, 'text_color': TEXT_COLOR, + 'text_font': f'montserrat_{font}', 'long_mode': 'DOT', + 'gesture_bubble': True, **kwargs}} + + +def card(pid, name, title, x, y, width=238, height=104, font=26): + return {'obj': {'x': x, 'y': y, 'width': width, 'height': height, + 'bg_color': CARD, 'radius': 16, 'border_width': 0, + 'pad_all': 0, 'scrollable': False, 'gesture_bubble': True, + 'widgets': [ + label(title, 16, 12, width - 32, 14, text_color=MUTED), + label('--', 16, 38, width - 32, font, id=f'{pid}_{name}_label', height=height - 42), + ]}} + + +def page_for(printer, index, total): + pid = printer['id'] + widgets = [ + label(printer['name'], 24, 16, 545, 28), + label(printer['subtitle'], 24, 53, 545, 14, text_color=MUTED), + {'obj': {'x': 586, 'y': 20, 'width': 190, 'height': 42, + 'bg_color': CARD, 'radius': 21, 'border_width': 0, 'pad_all': 0, + 'scrollable': False, 'gesture_bubble': True, + 'widgets': [label('SYNCING', 0, 11, 190, 16, + id=f'{pid}_status_label', text_align='CENTER', text_color=MUTED)]}}, + label('Waiting for Home Assistant', 24, 88, 752, 20, id=f'{pid}_task_label'), + {'arc': {'id': f'{pid}_progress_arc', 'x': 28, 'y': 132, 'width': 224, 'height': 224, + 'min_value': 0, 'max_value': 100, 'value': 0, + 'start_angle': 135, 'end_angle': 45, 'arc_width': 14, + 'arc_color': TRACK, 'indicator': {'arc_color': GREEN, 'arc_width': 14}, + 'adjustable': False, 'gesture_bubble': True}}, + label('--', 42, 207, 196, 48, id=f'{pid}_progress_label', text_align='CENTER'), + label('PRINT PROGRESS', 42, 266, 196, 14, text_align='CENTER', text_color=MUTED), + card(pid, 'nozzle', 'NOZZLES L/R / TARGET' if printer.get('dual_nozzle') + else 'NOZZLE / TARGET', 282, 132, height=118, + font=18 if printer.get('dual_nozzle') else 26), + card(pid, 'bed', 'BED / TARGET', 538, 132, height=118), + card(pid, 'remaining', 'REMAINING', 282, 262, height=86), + card(pid, 'layers', 'LAYER', 538, 262, height=86), + label('Telemetry unavailable', 24, 358, 752, 16, id=f'{pid}_stage_label', text_color=MUTED), + label('FILAMENT / ACTIVE SLOT OUTLINED IN GREEN', 24, 383, 752, 12, text_color=MUTED), + ] + trays = trays_for(printer) + if trays: + width = (752 - 8 * (len(trays) - 1)) // len(trays) + for i, tray in enumerate(trays): + widgets.append({'obj': { + 'id': f'{pid}_tray_{i}_card', 'x': 24 + (width + 8) * i, 'y': 401, + 'width': width, 'height': 32, 'bg_color': CARD, 'radius': 6, + 'border_width': 1, 'border_color': TRACK, 'pad_all': 0, + 'scrollable': False, 'gesture_bubble': True, + 'widgets': [ + {'obj': {'id': f'{pid}_tray_{i}_swatch', 'x': 1, 'y': 1, 'width': width - 4, + 'height': 5, 'bg_color': TRACK, 'radius': 2, 'border_width': 0, + 'pad_all': 0, 'scrollable': False, 'gesture_bubble': True}}, + label(f'{tray["label"]} --', 6, 11, width - 12, 12, + id=f'{pid}_tray_{i}_label'), + ]}}) + else: + widgets.append(label('No filament slots configured', 24, 403, 752, 14, text_color=MUTED)) + for direction, x, text in (('previous', 24, '< PREV'), ('next', 688, 'NEXT >')): + widgets.append({'button': {'x': x, 'y': 444, 'width': 88, 'height': 30, + 'bg_color': CARD, 'radius': 8, 'border_width': 0, + 'pad_all': 0, 'gesture_bubble': True, + 'widgets': [{'label': {'text': text, 'align': 'CENTER', + 'text_font': 'montserrat_12'}}], + 'on_click': [{f'lvgl.page.{direction}': {}}]}}) + widgets.append(label(f'{index + 1} / {total} SWIPE LEFT / RIGHT', 130, 451, 540, 14, + text_align='CENTER', text_color=MUTED)) + return {'id': f'{pid}_page', 'bg_color': BG, 'bg_opa': 'COVER', + 'pad_all': 0, 'scrollable': False, 'scrollbar_mode': 'OFF', + 'on_swipe_left': [ + {'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')}, + {'lvgl.page.next': {'animation': 'MOVE_LEFT', 'time': '180ms'}}], + 'on_swipe_right': [ + {'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')}, + {'lvgl.page.previous': {'animation': 'MOVE_RIGHT', 'time': '180ms'}}], + 'on_load': [{'script.execute': f'{pid}_refresh'}], 'widgets': widgets} + + +def generate(printers): + validate_printers(printers) + package = {'sensor': [], 'text_sensor': [], 'script': [], + 'interval': [{'interval': '1s', 'then': []}], 'lvgl': {'pages': []}} + for index, printer in enumerate(printers): + pid = printer['id'] + fields = dict(NUMERIC) + if printer.get('dual_nozzle'): + fields.update(DUAL) + expressions = {} + for key, suffix in fields.items(): + sid = f'{pid}_{key}' + eid = entity(printer, key, suffix) + 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()}) + 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') + trays = trays_for(printer) + for i, tray in enumerate(trays): + for key, attr in [('type', 'type'), ('color', 'color'), ('active', 'active')]: + text_fields[f'tray_{i}_{key}'] = (tray['entity'], attr) + for key, (eid, attribute) in text_fields.items(): + sid = f'{pid}_{key}' + subscription = {'platform': 'homeassistant', 'id': sid, 'entity_id': eid, 'internal': True} + if attribute: + subscription['attribute'] = attribute + package['text_sensor'].append(subscription) + expressions[key] = f'id({sid}).state' + lines = ['bambu_display::Snapshot data;', + 'data.connected = id(ha_api).is_connected_with_state_subscription();'] + for key, expr in expressions.items(): + if not key.startswith('tray_'): + lines.append(f'data.{key} = {expr};') + if printer.get('dual_nozzle'): + lines.append('data.dual_nozzle = true;') + tray_widgets = [] + for i, tray in enumerate(trays): + lines.append('data.trays.push_back({' + ', '.join([ + json.dumps(tray['label']), expressions[f'tray_{i}_type'], + expressions[f'tray_{i}_color'], expressions[f'tray_{i}_active']]) + '});') + tray_widgets.append('{' + ', '.join(f'id({pid}_tray_{i}_{part})' + for part in ('card', 'swatch', 'label')) + '}') + refs = [f'id({pid}_{key})' for key in ( + 'status_label', 'task_label', 'progress_arc', 'progress_label', 'nozzle_label', + 'bed_label', 'remaining_label', 'layers_label', 'stage_label')] + refs.append('{' + ', '.join(tray_widgets) + '}') + 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': { + 'condition': {'lvgl.page.is_showing': f'{pid}_page'}, + 'then': [{'script.execute': f'{pid}_refresh'}]}}) + package['lvgl']['pages'].append(page_for(printer, index, len(printers))) + return package + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', action='store_true', help='Fail if generated.yaml is stale') + args = parser.parse_args() + printers = yaml.safe_load((HERE / 'printers.yaml').read_text())['printers'] + package = generate(printers) + content = ('# AUTO-GENERATED by bambu-display/generate.py. Edit printers.yaml or generate.py.\n' + + yaml.dump(package, Dumper=Dumper, sort_keys=False, allow_unicode=True, width=110)) + destination = HERE / 'generated.yaml' + if args.check: + if not destination.exists() or destination.read_text() != content: + raise SystemExit('generated.yaml is stale; run generate.py') + print(f'Generated configuration is current ({len(printers)} printer pages).') + else: + destination.write_text(content) + print(f'Generated {destination.name} with {len(printers)} printer pages.') + + +if __name__ == '__main__': + main() diff --git a/esphome/bambu-display/generated.yaml b/esphome/bambu-display/generated.yaml new file mode 100644 index 0000000..b9890d9 --- /dev/null +++ b/esphome/bambu-display/generated.yaml @@ -0,0 +1,3238 @@ +# AUTO-GENERATED by bambu-display/generate.py. Edit printers.yaml or generate.py. +sensor: +- platform: homeassistant + id: prints_charming_progress + entity_id: sensor.p1s_01p09c470102673_print_progress + internal: true +- platform: homeassistant + id: prints_charming_nozzle + entity_id: sensor.p1s_01p09c470102673_nozzle_temperature + internal: true +- platform: homeassistant + id: prints_charming_nozzle_target + entity_id: sensor.p1s_01p09c470102673_nozzle_target_temperature + internal: true +- platform: homeassistant + id: prints_charming_bed + entity_id: sensor.p1s_01p09c470102673_bed_temperature + internal: true +- platform: homeassistant + id: prints_charming_bed_target + entity_id: sensor.p1s_01p09c470102673_bed_target_temperature + internal: true +- platform: homeassistant + id: prints_charming_time_left + entity_id: sensor.p1s_01p09c470102673_remaining_time + internal: true +- platform: homeassistant + id: prints_charming_layer + entity_id: sensor.p1s_01p09c470102673_current_layer + internal: true +- platform: homeassistant + id: prints_charming_total_layers + entity_id: sensor.p1s_01p09c470102673_total_layer_count + internal: true +- platform: homeassistant + id: prints_charming_fan + entity_id: sensor.p1s_01p09c470102673_cooling_fan_speed + internal: true +- platform: homeassistant + id: polly_pocket_progress + entity_id: sensor.a1mini_0309ca580403653_print_progress + internal: true +- platform: homeassistant + id: polly_pocket_nozzle + entity_id: sensor.a1mini_0309ca580403653_nozzle_temperature + internal: true +- platform: homeassistant + id: polly_pocket_nozzle_target + entity_id: sensor.a1mini_0309ca580403653_nozzle_target_temperature + internal: true +- platform: homeassistant + id: polly_pocket_bed + entity_id: sensor.a1mini_0309ca580403653_bed_temperature + internal: true +- platform: homeassistant + id: polly_pocket_bed_target + entity_id: sensor.a1mini_0309ca580403653_bed_target_temperature + internal: true +- platform: homeassistant + id: polly_pocket_time_left + entity_id: sensor.a1mini_0309ca580403653_remaining_time + internal: true +- platform: homeassistant + id: polly_pocket_layer + entity_id: sensor.a1mini_0309ca580403653_current_layer + internal: true +- platform: homeassistant + id: polly_pocket_total_layers + entity_id: sensor.a1mini_0309ca580403653_total_layer_count + internal: true +- platform: homeassistant + id: polly_pocket_fan + entity_id: sensor.a1mini_0309ca580403653_cooling_fan_speed + internal: true +- platform: homeassistant + id: ds_turd_flinger_progress + entity_id: sensor.a1_03919d540806387_print_progress + internal: true +- platform: homeassistant + id: ds_turd_flinger_nozzle + entity_id: sensor.a1_03919d540806387_nozzle_temperature + internal: true +- platform: homeassistant + id: ds_turd_flinger_nozzle_target + entity_id: sensor.a1_03919d540806387_nozzle_target_temperature + internal: true +- platform: homeassistant + id: ds_turd_flinger_bed + entity_id: sensor.a1_03919d540806387_bed_temperature + internal: true +- platform: homeassistant + id: ds_turd_flinger_bed_target + entity_id: sensor.a1_03919d540806387_bed_target_temperature + internal: true +- platform: homeassistant + id: ds_turd_flinger_time_left + entity_id: sensor.a1_03919d540806387_remaining_time + internal: true +- platform: homeassistant + id: ds_turd_flinger_layer + entity_id: sensor.a1_03919d540806387_current_layer + internal: true +- platform: homeassistant + id: ds_turd_flinger_total_layers + entity_id: sensor.a1_03919d540806387_total_layer_count + internal: true +- platform: homeassistant + id: ds_turd_flinger_fan + entity_id: sensor.a1_03919d540806387_cooling_fan_speed + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_progress + entity_id: sensor.jordyns_layer_slayer_print_progress + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_nozzle + entity_id: sensor.jordyns_layer_slayer_nozzle_temperature + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_nozzle_target + entity_id: sensor.jordyns_layer_slayer_nozzle_target_temperature + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_bed + entity_id: sensor.jordyns_layer_slayer_bed_temperature + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_bed_target + entity_id: sensor.jordyns_layer_slayer_bed_target_temperature + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_time_left + entity_id: sensor.jordyns_layer_slayer_remaining_time + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_layer + entity_id: sensor.jordyns_layer_slayer_current_layer + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_total_layers + entity_id: sensor.jordyns_layer_slayer_total_layer_count + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_fan + entity_id: sensor.jordyns_layer_slayer_cooling_fan_speed + internal: true +- platform: homeassistant + id: constipation_orion_progress + entity_id: sensor.constipation_orion_print_progress + internal: true +- platform: homeassistant + id: constipation_orion_nozzle + entity_id: sensor.constipation_orion_nozzle_temperature + internal: true +- platform: homeassistant + id: constipation_orion_nozzle_target + entity_id: sensor.constipation_orion_nozzle_target_temperature + internal: true +- platform: homeassistant + id: constipation_orion_bed + entity_id: sensor.constipation_orion_bed_temperature + internal: true +- platform: homeassistant + id: constipation_orion_bed_target + entity_id: sensor.constipation_orion_bed_target_temperature + internal: true +- platform: homeassistant + id: constipation_orion_time_left + entity_id: sensor.constipation_orion_remaining_time + internal: true +- platform: homeassistant + id: constipation_orion_layer + entity_id: sensor.constipation_orion_current_layer + internal: true +- platform: homeassistant + id: constipation_orion_total_layers + entity_id: sensor.constipation_orion_total_layer_count + internal: true +- platform: homeassistant + id: constipation_orion_fan + entity_id: sensor.constipation_orion_cooling_fan_speed + internal: true +- platform: homeassistant + id: constipation_orion_left_nozzle + entity_id: sensor.constipation_orion_left_nozzle_temperature + internal: true +- platform: homeassistant + id: constipation_orion_left_target + entity_id: sensor.constipation_orion_left_nozzle_target_temperature + internal: true +- platform: homeassistant + id: constipation_orion_right_nozzle + entity_id: sensor.constipation_orion_right_nozzle_temperature + internal: true +- platform: homeassistant + id: constipation_orion_right_target + entity_id: sensor.constipation_orion_right_nozzle_target_temperature + internal: true +text_sensor: +- platform: homeassistant + id: prints_charming_status + entity_id: sensor.p1s_01p09c470102673_print_status + internal: true +- platform: homeassistant + id: prints_charming_task + entity_id: sensor.p1s_01p09c470102673_task_name + internal: true +- platform: homeassistant + id: prints_charming_stage + entity_id: sensor.p1s_01p09c470102673_current_stage + internal: true +- platform: homeassistant + id: prints_charming_online + entity_id: binary_sensor.p1s_01p09c470102673_online + internal: true +- platform: homeassistant + id: prints_charming_print_error + entity_id: binary_sensor.p1s_01p09c470102673_print_error + internal: true +- platform: homeassistant + id: prints_charming_hms_error + entity_id: binary_sensor.p1s_01p09c470102673_hms_errors + internal: true +- platform: homeassistant + id: prints_charming_remaining_unit + entity_id: sensor.p1s_01p09c470102673_remaining_time + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: prints_charming_nozzle_unit + entity_id: sensor.p1s_01p09c470102673_nozzle_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: prints_charming_bed_unit + entity_id: sensor.p1s_01p09c470102673_bed_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: prints_charming_tray_0_type + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_1 + internal: true + attribute: type +- platform: homeassistant + id: prints_charming_tray_0_color + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_1 + internal: true + attribute: color +- platform: homeassistant + id: prints_charming_tray_0_active + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_1 + internal: true + attribute: active +- platform: homeassistant + id: prints_charming_tray_1_type + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_2 + internal: true + attribute: type +- platform: homeassistant + id: prints_charming_tray_1_color + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_2 + internal: true + attribute: color +- platform: homeassistant + id: prints_charming_tray_1_active + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_2 + internal: true + attribute: active +- platform: homeassistant + id: prints_charming_tray_2_type + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_3 + internal: true + attribute: type +- platform: homeassistant + id: prints_charming_tray_2_color + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_3 + internal: true + attribute: color +- platform: homeassistant + id: prints_charming_tray_2_active + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_3 + internal: true + attribute: active +- platform: homeassistant + id: prints_charming_tray_3_type + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_4 + internal: true + attribute: type +- platform: homeassistant + id: prints_charming_tray_3_color + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_4 + internal: true + attribute: color +- platform: homeassistant + id: prints_charming_tray_3_active + entity_id: sensor.p1s_01p09c470102673_ams_1_tray_4 + internal: true + attribute: active +- platform: homeassistant + id: prints_charming_tray_4_type + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_1 + internal: true + attribute: type +- platform: homeassistant + id: prints_charming_tray_4_color + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_1 + internal: true + attribute: color +- platform: homeassistant + id: prints_charming_tray_4_active + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_1 + internal: true + attribute: active +- platform: homeassistant + id: prints_charming_tray_5_type + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_2 + internal: true + attribute: type +- platform: homeassistant + id: prints_charming_tray_5_color + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_2 + internal: true + attribute: color +- platform: homeassistant + id: prints_charming_tray_5_active + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_2 + internal: true + attribute: active +- platform: homeassistant + id: prints_charming_tray_6_type + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_3 + internal: true + attribute: type +- platform: homeassistant + id: prints_charming_tray_6_color + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_3 + internal: true + attribute: color +- platform: homeassistant + id: prints_charming_tray_6_active + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_3 + internal: true + attribute: active +- platform: homeassistant + id: prints_charming_tray_7_type + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_4 + internal: true + attribute: type +- platform: homeassistant + id: prints_charming_tray_7_color + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_4 + internal: true + attribute: color +- platform: homeassistant + id: prints_charming_tray_7_active + entity_id: sensor.p1s_01p09c470102673_ams_2_tray_4 + internal: true + attribute: active +- platform: homeassistant + id: polly_pocket_status + entity_id: sensor.a1mini_0309ca580403653_print_status + internal: true +- platform: homeassistant + id: polly_pocket_task + entity_id: sensor.a1mini_0309ca580403653_task_name + internal: true +- platform: homeassistant + id: polly_pocket_stage + entity_id: sensor.a1mini_0309ca580403653_current_stage + internal: true +- platform: homeassistant + id: polly_pocket_online + entity_id: binary_sensor.a1mini_0309ca580403653_online + internal: true +- platform: homeassistant + id: polly_pocket_print_error + entity_id: binary_sensor.a1mini_0309ca580403653_print_error + internal: true +- platform: homeassistant + id: polly_pocket_hms_error + entity_id: binary_sensor.a1mini_0309ca580403653_hms_errors + internal: true +- platform: homeassistant + id: polly_pocket_remaining_unit + entity_id: sensor.a1mini_0309ca580403653_remaining_time + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: polly_pocket_nozzle_unit + entity_id: sensor.a1mini_0309ca580403653_nozzle_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: polly_pocket_bed_unit + entity_id: sensor.a1mini_0309ca580403653_bed_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: polly_pocket_tray_0_type + entity_id: sensor.a1mini_0309ca580403653_externalspool_external_spool + internal: true + attribute: type +- platform: homeassistant + id: polly_pocket_tray_0_color + entity_id: sensor.a1mini_0309ca580403653_externalspool_external_spool + internal: true + attribute: color +- platform: homeassistant + id: polly_pocket_tray_0_active + entity_id: sensor.a1mini_0309ca580403653_externalspool_external_spool + internal: true + attribute: active +- platform: homeassistant + id: ds_turd_flinger_status + entity_id: sensor.a1_03919d540806387_print_status + internal: true +- platform: homeassistant + id: ds_turd_flinger_task + entity_id: sensor.a1_03919d540806387_task_name + internal: true +- platform: homeassistant + id: ds_turd_flinger_stage + entity_id: sensor.a1_03919d540806387_current_stage + internal: true +- platform: homeassistant + id: ds_turd_flinger_online + entity_id: binary_sensor.a1_03919d540806387_online + internal: true +- platform: homeassistant + id: ds_turd_flinger_print_error + entity_id: binary_sensor.a1_03919d540806387_print_error + internal: true +- platform: homeassistant + id: ds_turd_flinger_hms_error + entity_id: binary_sensor.a1_03919d540806387_hms_errors + internal: true +- platform: homeassistant + id: ds_turd_flinger_remaining_unit + entity_id: sensor.a1_03919d540806387_remaining_time + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: ds_turd_flinger_nozzle_unit + entity_id: sensor.a1_03919d540806387_nozzle_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: ds_turd_flinger_bed_unit + entity_id: sensor.a1_03919d540806387_bed_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: ds_turd_flinger_tray_0_type + entity_id: sensor.a1_03919d540806387_ams_1_tray_1 + internal: true + attribute: type +- platform: homeassistant + id: ds_turd_flinger_tray_0_color + entity_id: sensor.a1_03919d540806387_ams_1_tray_1 + internal: true + attribute: color +- platform: homeassistant + id: ds_turd_flinger_tray_0_active + entity_id: sensor.a1_03919d540806387_ams_1_tray_1 + internal: true + attribute: active +- platform: homeassistant + id: ds_turd_flinger_tray_1_type + entity_id: sensor.a1_03919d540806387_ams_1_tray_2 + internal: true + attribute: type +- platform: homeassistant + id: ds_turd_flinger_tray_1_color + entity_id: sensor.a1_03919d540806387_ams_1_tray_2 + internal: true + attribute: color +- platform: homeassistant + id: ds_turd_flinger_tray_1_active + entity_id: sensor.a1_03919d540806387_ams_1_tray_2 + internal: true + attribute: active +- platform: homeassistant + id: ds_turd_flinger_tray_2_type + entity_id: sensor.a1_03919d540806387_ams_1_tray_3 + internal: true + attribute: type +- platform: homeassistant + id: ds_turd_flinger_tray_2_color + entity_id: sensor.a1_03919d540806387_ams_1_tray_3 + internal: true + attribute: color +- platform: homeassistant + id: ds_turd_flinger_tray_2_active + entity_id: sensor.a1_03919d540806387_ams_1_tray_3 + internal: true + attribute: active +- platform: homeassistant + id: ds_turd_flinger_tray_3_type + entity_id: sensor.a1_03919d540806387_ams_1_tray_4 + internal: true + attribute: type +- platform: homeassistant + id: ds_turd_flinger_tray_3_color + entity_id: sensor.a1_03919d540806387_ams_1_tray_4 + internal: true + attribute: color +- platform: homeassistant + id: ds_turd_flinger_tray_3_active + entity_id: sensor.a1_03919d540806387_ams_1_tray_4 + internal: true + attribute: active +- platform: homeassistant + id: jordyns_layer_slayer_status + entity_id: sensor.jordyns_layer_slayer_print_status + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_task + entity_id: sensor.jordyns_layer_slayer_task_name + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_stage + entity_id: sensor.jordyns_layer_slayer_current_stage + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_online + entity_id: binary_sensor.jordyns_layer_slayer_online + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_print_error + entity_id: binary_sensor.jordyns_layer_slayer_print_error + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_hms_error + entity_id: binary_sensor.jordyns_layer_slayer_hms_errors + internal: true +- platform: homeassistant + id: jordyns_layer_slayer_remaining_unit + entity_id: sensor.jordyns_layer_slayer_remaining_time + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: jordyns_layer_slayer_nozzle_unit + entity_id: sensor.jordyns_layer_slayer_nozzle_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: jordyns_layer_slayer_bed_unit + entity_id: sensor.jordyns_layer_slayer_bed_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: jordyns_layer_slayer_tray_0_type + entity_id: sensor.jordyns_layer_slayer_ams_tray_1 + internal: true + attribute: type +- platform: homeassistant + id: jordyns_layer_slayer_tray_0_color + entity_id: sensor.jordyns_layer_slayer_ams_tray_1 + internal: true + attribute: color +- platform: homeassistant + id: jordyns_layer_slayer_tray_0_active + entity_id: sensor.jordyns_layer_slayer_ams_tray_1 + internal: true + attribute: active +- platform: homeassistant + id: jordyns_layer_slayer_tray_1_type + entity_id: sensor.jordyns_layer_slayer_ams_tray_2 + internal: true + attribute: type +- platform: homeassistant + id: jordyns_layer_slayer_tray_1_color + entity_id: sensor.jordyns_layer_slayer_ams_tray_2 + internal: true + attribute: color +- platform: homeassistant + id: jordyns_layer_slayer_tray_1_active + entity_id: sensor.jordyns_layer_slayer_ams_tray_2 + internal: true + attribute: active +- platform: homeassistant + id: jordyns_layer_slayer_tray_2_type + entity_id: sensor.jordyns_layer_slayer_ams_tray_3 + internal: true + attribute: type +- platform: homeassistant + id: jordyns_layer_slayer_tray_2_color + entity_id: sensor.jordyns_layer_slayer_ams_tray_3 + internal: true + attribute: color +- platform: homeassistant + id: jordyns_layer_slayer_tray_2_active + entity_id: sensor.jordyns_layer_slayer_ams_tray_3 + internal: true + attribute: active +- platform: homeassistant + id: jordyns_layer_slayer_tray_3_type + entity_id: sensor.jordyns_layer_slayer_ams_tray_4 + internal: true + attribute: type +- platform: homeassistant + id: jordyns_layer_slayer_tray_3_color + entity_id: sensor.jordyns_layer_slayer_ams_tray_4 + internal: true + attribute: color +- platform: homeassistant + id: jordyns_layer_slayer_tray_3_active + entity_id: sensor.jordyns_layer_slayer_ams_tray_4 + internal: true + attribute: active +- platform: homeassistant + id: constipation_orion_status + entity_id: sensor.constipation_orion_print_status + internal: true +- platform: homeassistant + id: constipation_orion_task + entity_id: sensor.constipation_orion_task_name + internal: true +- platform: homeassistant + id: constipation_orion_stage + entity_id: sensor.constipation_orion_current_stage + internal: true +- platform: homeassistant + id: constipation_orion_online + entity_id: binary_sensor.constipation_orion_online + internal: true +- platform: homeassistant + id: constipation_orion_print_error + entity_id: binary_sensor.constipation_orion_print_error + internal: true +- platform: homeassistant + id: constipation_orion_hms_error + entity_id: binary_sensor.constipation_orion_hms_errors + internal: true +- platform: homeassistant + id: constipation_orion_remaining_unit + entity_id: sensor.constipation_orion_remaining_time + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: constipation_orion_nozzle_unit + entity_id: sensor.constipation_orion_nozzle_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: constipation_orion_bed_unit + entity_id: sensor.constipation_orion_bed_temperature + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: constipation_orion_tray_0_type + entity_id: sensor.constipation_orion_ams_1_tray_1 + internal: true + attribute: type +- platform: homeassistant + id: constipation_orion_tray_0_color + entity_id: sensor.constipation_orion_ams_1_tray_1 + internal: true + attribute: color +- platform: homeassistant + id: constipation_orion_tray_0_active + entity_id: sensor.constipation_orion_ams_1_tray_1 + internal: true + attribute: active +- platform: homeassistant + id: constipation_orion_tray_1_type + entity_id: sensor.constipation_orion_ams_1_tray_2 + internal: true + attribute: type +- platform: homeassistant + id: constipation_orion_tray_1_color + entity_id: sensor.constipation_orion_ams_1_tray_2 + internal: true + attribute: color +- platform: homeassistant + id: constipation_orion_tray_1_active + entity_id: sensor.constipation_orion_ams_1_tray_2 + internal: true + attribute: active +- platform: homeassistant + id: constipation_orion_tray_2_type + entity_id: sensor.constipation_orion_ams_1_tray_3 + internal: true + attribute: type +- platform: homeassistant + id: constipation_orion_tray_2_color + entity_id: sensor.constipation_orion_ams_1_tray_3 + internal: true + attribute: color +- platform: homeassistant + id: constipation_orion_tray_2_active + entity_id: sensor.constipation_orion_ams_1_tray_3 + internal: true + attribute: active +- platform: homeassistant + id: constipation_orion_tray_3_type + entity_id: sensor.constipation_orion_ams_1_tray_4 + internal: true + attribute: type +- platform: homeassistant + id: constipation_orion_tray_3_color + entity_id: sensor.constipation_orion_ams_1_tray_4 + internal: true + attribute: color +- platform: homeassistant + id: constipation_orion_tray_3_active + entity_id: sensor.constipation_orion_ams_1_tray_4 + internal: true + attribute: active +script: +- id: prints_charming_refresh + then: + - lambda: !lambda |- + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(prints_charming_progress).state; + data.nozzle = id(prints_charming_nozzle).state; + data.nozzle_target = id(prints_charming_nozzle_target).state; + data.bed = id(prints_charming_bed).state; + data.bed_target = id(prints_charming_bed_target).state; + data.time_left = id(prints_charming_time_left).state; + data.layer = id(prints_charming_layer).state; + data.total_layers = id(prints_charming_total_layers).state; + data.fan = id(prints_charming_fan).state; + data.status = id(prints_charming_status).state; + data.task = id(prints_charming_task).state; + data.stage = id(prints_charming_stage).state; + data.online = id(prints_charming_online).state; + data.print_error = id(prints_charming_print_error).state; + data.hms_error = id(prints_charming_hms_error).state; + data.remaining_unit = id(prints_charming_remaining_unit).state; + data.nozzle_unit = id(prints_charming_nozzle_unit).state; + data.bed_unit = id(prints_charming_bed_unit).state; + data.trays.push_back({"A1", id(prints_charming_tray_0_type).state, id(prints_charming_tray_0_color).state, id(prints_charming_tray_0_active).state}); + data.trays.push_back({"A2", id(prints_charming_tray_1_type).state, id(prints_charming_tray_1_color).state, id(prints_charming_tray_1_active).state}); + data.trays.push_back({"A3", id(prints_charming_tray_2_type).state, id(prints_charming_tray_2_color).state, id(prints_charming_tray_2_active).state}); + data.trays.push_back({"A4", id(prints_charming_tray_3_type).state, id(prints_charming_tray_3_color).state, id(prints_charming_tray_3_active).state}); + data.trays.push_back({"B1", id(prints_charming_tray_4_type).state, id(prints_charming_tray_4_color).state, id(prints_charming_tray_4_active).state}); + data.trays.push_back({"B2", id(prints_charming_tray_5_type).state, id(prints_charming_tray_5_color).state, id(prints_charming_tray_5_active).state}); + data.trays.push_back({"B3", id(prints_charming_tray_6_type).state, id(prints_charming_tray_6_color).state, id(prints_charming_tray_6_active).state}); + data.trays.push_back({"B4", id(prints_charming_tray_7_type).state, id(prints_charming_tray_7_color).state, id(prints_charming_tray_7_active).state}); + bambu_display::render(data, {id(prints_charming_status_label), id(prints_charming_task_label), id(prints_charming_progress_arc), id(prints_charming_progress_label), id(prints_charming_nozzle_label), id(prints_charming_bed_label), id(prints_charming_remaining_label), id(prints_charming_layers_label), id(prints_charming_stage_label), {{id(prints_charming_tray_0_card), id(prints_charming_tray_0_swatch), id(prints_charming_tray_0_label)}, {id(prints_charming_tray_1_card), id(prints_charming_tray_1_swatch), id(prints_charming_tray_1_label)}, {id(prints_charming_tray_2_card), id(prints_charming_tray_2_swatch), id(prints_charming_tray_2_label)}, {id(prints_charming_tray_3_card), id(prints_charming_tray_3_swatch), id(prints_charming_tray_3_label)}, {id(prints_charming_tray_4_card), id(prints_charming_tray_4_swatch), id(prints_charming_tray_4_label)}, {id(prints_charming_tray_5_card), id(prints_charming_tray_5_swatch), id(prints_charming_tray_5_label)}, {id(prints_charming_tray_6_card), id(prints_charming_tray_6_swatch), id(prints_charming_tray_6_label)}, {id(prints_charming_tray_7_card), id(prints_charming_tray_7_swatch), id(prints_charming_tray_7_label)}}}); +- id: polly_pocket_refresh + then: + - lambda: !lambda |- + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(polly_pocket_progress).state; + data.nozzle = id(polly_pocket_nozzle).state; + data.nozzle_target = id(polly_pocket_nozzle_target).state; + data.bed = id(polly_pocket_bed).state; + data.bed_target = id(polly_pocket_bed_target).state; + data.time_left = id(polly_pocket_time_left).state; + data.layer = id(polly_pocket_layer).state; + data.total_layers = id(polly_pocket_total_layers).state; + data.fan = id(polly_pocket_fan).state; + data.status = id(polly_pocket_status).state; + data.task = id(polly_pocket_task).state; + data.stage = id(polly_pocket_stage).state; + data.online = id(polly_pocket_online).state; + data.print_error = id(polly_pocket_print_error).state; + data.hms_error = id(polly_pocket_hms_error).state; + data.remaining_unit = id(polly_pocket_remaining_unit).state; + data.nozzle_unit = id(polly_pocket_nozzle_unit).state; + data.bed_unit = id(polly_pocket_bed_unit).state; + data.trays.push_back({"EXT", id(polly_pocket_tray_0_type).state, id(polly_pocket_tray_0_color).state, id(polly_pocket_tray_0_active).state}); + bambu_display::render(data, {id(polly_pocket_status_label), id(polly_pocket_task_label), id(polly_pocket_progress_arc), id(polly_pocket_progress_label), id(polly_pocket_nozzle_label), id(polly_pocket_bed_label), id(polly_pocket_remaining_label), id(polly_pocket_layers_label), id(polly_pocket_stage_label), {{id(polly_pocket_tray_0_card), id(polly_pocket_tray_0_swatch), id(polly_pocket_tray_0_label)}}}); +- id: ds_turd_flinger_refresh + then: + - lambda: !lambda |- + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(ds_turd_flinger_progress).state; + data.nozzle = id(ds_turd_flinger_nozzle).state; + data.nozzle_target = id(ds_turd_flinger_nozzle_target).state; + data.bed = id(ds_turd_flinger_bed).state; + data.bed_target = id(ds_turd_flinger_bed_target).state; + data.time_left = id(ds_turd_flinger_time_left).state; + data.layer = id(ds_turd_flinger_layer).state; + data.total_layers = id(ds_turd_flinger_total_layers).state; + data.fan = id(ds_turd_flinger_fan).state; + data.status = id(ds_turd_flinger_status).state; + data.task = id(ds_turd_flinger_task).state; + data.stage = id(ds_turd_flinger_stage).state; + data.online = id(ds_turd_flinger_online).state; + data.print_error = id(ds_turd_flinger_print_error).state; + data.hms_error = id(ds_turd_flinger_hms_error).state; + data.remaining_unit = id(ds_turd_flinger_remaining_unit).state; + data.nozzle_unit = id(ds_turd_flinger_nozzle_unit).state; + data.bed_unit = id(ds_turd_flinger_bed_unit).state; + data.trays.push_back({"A1", id(ds_turd_flinger_tray_0_type).state, id(ds_turd_flinger_tray_0_color).state, id(ds_turd_flinger_tray_0_active).state}); + data.trays.push_back({"A2", id(ds_turd_flinger_tray_1_type).state, id(ds_turd_flinger_tray_1_color).state, id(ds_turd_flinger_tray_1_active).state}); + data.trays.push_back({"A3", id(ds_turd_flinger_tray_2_type).state, id(ds_turd_flinger_tray_2_color).state, id(ds_turd_flinger_tray_2_active).state}); + data.trays.push_back({"A4", id(ds_turd_flinger_tray_3_type).state, id(ds_turd_flinger_tray_3_color).state, id(ds_turd_flinger_tray_3_active).state}); + bambu_display::render(data, {id(ds_turd_flinger_status_label), id(ds_turd_flinger_task_label), id(ds_turd_flinger_progress_arc), id(ds_turd_flinger_progress_label), id(ds_turd_flinger_nozzle_label), id(ds_turd_flinger_bed_label), id(ds_turd_flinger_remaining_label), id(ds_turd_flinger_layers_label), id(ds_turd_flinger_stage_label), {{id(ds_turd_flinger_tray_0_card), id(ds_turd_flinger_tray_0_swatch), id(ds_turd_flinger_tray_0_label)}, {id(ds_turd_flinger_tray_1_card), id(ds_turd_flinger_tray_1_swatch), id(ds_turd_flinger_tray_1_label)}, {id(ds_turd_flinger_tray_2_card), id(ds_turd_flinger_tray_2_swatch), id(ds_turd_flinger_tray_2_label)}, {id(ds_turd_flinger_tray_3_card), id(ds_turd_flinger_tray_3_swatch), id(ds_turd_flinger_tray_3_label)}}}); +- id: jordyns_layer_slayer_refresh + then: + - lambda: !lambda |- + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(jordyns_layer_slayer_progress).state; + data.nozzle = id(jordyns_layer_slayer_nozzle).state; + data.nozzle_target = id(jordyns_layer_slayer_nozzle_target).state; + data.bed = id(jordyns_layer_slayer_bed).state; + data.bed_target = id(jordyns_layer_slayer_bed_target).state; + data.time_left = id(jordyns_layer_slayer_time_left).state; + data.layer = id(jordyns_layer_slayer_layer).state; + data.total_layers = id(jordyns_layer_slayer_total_layers).state; + data.fan = id(jordyns_layer_slayer_fan).state; + data.status = id(jordyns_layer_slayer_status).state; + data.task = id(jordyns_layer_slayer_task).state; + data.stage = id(jordyns_layer_slayer_stage).state; + data.online = id(jordyns_layer_slayer_online).state; + data.print_error = id(jordyns_layer_slayer_print_error).state; + data.hms_error = id(jordyns_layer_slayer_hms_error).state; + data.remaining_unit = id(jordyns_layer_slayer_remaining_unit).state; + data.nozzle_unit = id(jordyns_layer_slayer_nozzle_unit).state; + data.bed_unit = id(jordyns_layer_slayer_bed_unit).state; + data.trays.push_back({"A1", id(jordyns_layer_slayer_tray_0_type).state, id(jordyns_layer_slayer_tray_0_color).state, id(jordyns_layer_slayer_tray_0_active).state}); + data.trays.push_back({"A2", id(jordyns_layer_slayer_tray_1_type).state, id(jordyns_layer_slayer_tray_1_color).state, id(jordyns_layer_slayer_tray_1_active).state}); + data.trays.push_back({"A3", id(jordyns_layer_slayer_tray_2_type).state, id(jordyns_layer_slayer_tray_2_color).state, id(jordyns_layer_slayer_tray_2_active).state}); + data.trays.push_back({"A4", id(jordyns_layer_slayer_tray_3_type).state, id(jordyns_layer_slayer_tray_3_color).state, id(jordyns_layer_slayer_tray_3_active).state}); + bambu_display::render(data, {id(jordyns_layer_slayer_status_label), id(jordyns_layer_slayer_task_label), id(jordyns_layer_slayer_progress_arc), id(jordyns_layer_slayer_progress_label), id(jordyns_layer_slayer_nozzle_label), id(jordyns_layer_slayer_bed_label), id(jordyns_layer_slayer_remaining_label), id(jordyns_layer_slayer_layers_label), id(jordyns_layer_slayer_stage_label), {{id(jordyns_layer_slayer_tray_0_card), id(jordyns_layer_slayer_tray_0_swatch), id(jordyns_layer_slayer_tray_0_label)}, {id(jordyns_layer_slayer_tray_1_card), id(jordyns_layer_slayer_tray_1_swatch), id(jordyns_layer_slayer_tray_1_label)}, {id(jordyns_layer_slayer_tray_2_card), id(jordyns_layer_slayer_tray_2_swatch), id(jordyns_layer_slayer_tray_2_label)}, {id(jordyns_layer_slayer_tray_3_card), id(jordyns_layer_slayer_tray_3_swatch), id(jordyns_layer_slayer_tray_3_label)}}}); +- id: constipation_orion_refresh + then: + - lambda: !lambda |- + bambu_display::Snapshot data; + data.connected = id(ha_api).is_connected_with_state_subscription(); + data.progress = id(constipation_orion_progress).state; + data.nozzle = id(constipation_orion_nozzle).state; + data.nozzle_target = id(constipation_orion_nozzle_target).state; + data.bed = id(constipation_orion_bed).state; + data.bed_target = id(constipation_orion_bed_target).state; + data.time_left = id(constipation_orion_time_left).state; + data.layer = id(constipation_orion_layer).state; + data.total_layers = id(constipation_orion_total_layers).state; + data.fan = id(constipation_orion_fan).state; + data.left_nozzle = id(constipation_orion_left_nozzle).state; + data.left_target = id(constipation_orion_left_target).state; + data.right_nozzle = id(constipation_orion_right_nozzle).state; + data.right_target = id(constipation_orion_right_target).state; + data.status = id(constipation_orion_status).state; + data.task = id(constipation_orion_task).state; + data.stage = id(constipation_orion_stage).state; + data.online = id(constipation_orion_online).state; + data.print_error = id(constipation_orion_print_error).state; + data.hms_error = id(constipation_orion_hms_error).state; + data.remaining_unit = id(constipation_orion_remaining_unit).state; + data.nozzle_unit = id(constipation_orion_nozzle_unit).state; + data.bed_unit = id(constipation_orion_bed_unit).state; + data.dual_nozzle = true; + data.trays.push_back({"A1", id(constipation_orion_tray_0_type).state, id(constipation_orion_tray_0_color).state, id(constipation_orion_tray_0_active).state}); + data.trays.push_back({"A2", id(constipation_orion_tray_1_type).state, id(constipation_orion_tray_1_color).state, id(constipation_orion_tray_1_active).state}); + data.trays.push_back({"A3", id(constipation_orion_tray_2_type).state, id(constipation_orion_tray_2_color).state, id(constipation_orion_tray_2_active).state}); + data.trays.push_back({"A4", id(constipation_orion_tray_3_type).state, id(constipation_orion_tray_3_color).state, id(constipation_orion_tray_3_active).state}); + bambu_display::render(data, {id(constipation_orion_status_label), id(constipation_orion_task_label), id(constipation_orion_progress_arc), id(constipation_orion_progress_label), id(constipation_orion_nozzle_label), id(constipation_orion_bed_label), id(constipation_orion_remaining_label), id(constipation_orion_layers_label), id(constipation_orion_stage_label), {{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)}}}); +interval: +- interval: 1s + then: + - if: + condition: + lvgl.page.is_showing: prints_charming_page + then: + - script.execute: prints_charming_refresh + - if: + condition: + lvgl.page.is_showing: polly_pocket_page + then: + - script.execute: polly_pocket_refresh + - if: + condition: + lvgl.page.is_showing: ds_turd_flinger_page + then: + - script.execute: ds_turd_flinger_refresh + - if: + condition: + lvgl.page.is_showing: jordyns_layer_slayer_page + then: + - script.execute: jordyns_layer_slayer_refresh + - if: + condition: + lvgl.page.is_showing: constipation_orion_page + then: + - script.execute: constipation_orion_refresh +lvgl: + pages: + - id: prints_charming_page + bg_color: 856859 + 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: prints_charming_refresh + widgets: + - label: + text: PrintsCharming + x: 24 + y: 16 + width: 545 + height: 36 + text_color: 15594487 + text_font: montserrat_28 + long_mode: DOT + gesture_bubble: true + - label: + text: P1S / Office + x: 24 + y: 53 + width: 545 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - obj: + x: 586 + y: 20 + width: 190 + height: 42 + bg_color: 1516078 + radius: 21 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: SYNCING + x: 0 + y: 11 + width: 190 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: prints_charming_status_label + text_align: CENTER + - label: + text: Waiting for Home Assistant + x: 24 + y: 88 + width: 752 + height: 28 + text_color: 15594487 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + id: prints_charming_task_label + - arc: + id: prints_charming_progress_arc + x: 28 + y: 132 + width: 224 + height: 224 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 14 + arc_color: 2503746 + indicator: + arc_color: 6348442 + arc_width: 14 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 42 + y: 207 + width: 196 + height: 56 + text_color: 15594487 + text_font: montserrat_48 + long_mode: DOT + gesture_bubble: true + id: prints_charming_progress_label + text_align: CENTER + - label: + text: PRINT PROGRESS + x: 42 + y: 266 + width: 196 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - obj: + x: 282 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: NOZZLE / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: prints_charming_nozzle_label + - obj: + x: 538 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: BED / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: prints_charming_bed_label + - obj: + x: 282 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: REMAINING + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: prints_charming_remaining_label + - obj: + x: 538 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: LAYER + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: prints_charming_layers_label + - label: + text: Telemetry unavailable + x: 24 + y: 358 + width: 752 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: prints_charming_stage_label + - label: + text: FILAMENT / ACTIVE SLOT OUTLINED IN GREEN + x: 24 + y: 383 + width: 752 + height: 20 + text_color: 9544629 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + - obj: + id: prints_charming_tray_0_card + x: 24 + y: 401 + width: 87 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: prints_charming_tray_0_swatch + x: 1 + y: 1 + width: 83 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A1 -- + x: 6 + y: 11 + width: 75 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_tray_0_label + - obj: + id: prints_charming_tray_1_card + x: 119 + y: 401 + width: 87 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: prints_charming_tray_1_swatch + x: 1 + y: 1 + width: 83 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A2 -- + x: 6 + y: 11 + width: 75 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_tray_1_label + - obj: + id: prints_charming_tray_2_card + x: 214 + y: 401 + width: 87 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: prints_charming_tray_2_swatch + x: 1 + y: 1 + width: 83 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A3 -- + x: 6 + y: 11 + width: 75 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_tray_2_label + - obj: + id: prints_charming_tray_3_card + x: 309 + y: 401 + width: 87 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: prints_charming_tray_3_swatch + x: 1 + y: 1 + width: 83 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A4 -- + x: 6 + y: 11 + width: 75 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_tray_3_label + - obj: + id: prints_charming_tray_4_card + x: 404 + y: 401 + width: 87 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: prints_charming_tray_4_swatch + x: 1 + y: 1 + width: 83 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: B1 -- + x: 6 + y: 11 + width: 75 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_tray_4_label + - obj: + id: prints_charming_tray_5_card + x: 499 + y: 401 + width: 87 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: prints_charming_tray_5_swatch + x: 1 + y: 1 + width: 83 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: B2 -- + x: 6 + y: 11 + width: 75 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_tray_5_label + - obj: + id: prints_charming_tray_6_card + x: 594 + y: 401 + width: 87 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: prints_charming_tray_6_swatch + x: 1 + y: 1 + width: 83 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: B3 -- + x: 6 + y: 11 + width: 75 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_tray_6_label + - obj: + id: prints_charming_tray_7_card + x: 689 + y: 401 + width: 87 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: prints_charming_tray_7_swatch + x: 1 + y: 1 + width: 83 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: B4 -- + x: 6 + y: 11 + width: 75 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: prints_charming_tray_7_label + - button: + x: 24 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: < PREV + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.previous: {} + - button: + x: 688 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: NEXT > + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.next: {} + - label: + text: 1 / 5 SWIPE LEFT / RIGHT + x: 130 + y: 451 + width: 540 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - id: polly_pocket_page + bg_color: 856859 + 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: polly_pocket_refresh + widgets: + - label: + text: Polly-Pocket + x: 24 + y: 16 + width: 545 + height: 36 + text_color: 15594487 + text_font: montserrat_28 + long_mode: DOT + gesture_bubble: true + - label: + text: A1 mini / Office + x: 24 + y: 53 + width: 545 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - obj: + x: 586 + y: 20 + width: 190 + height: 42 + bg_color: 1516078 + radius: 21 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: SYNCING + x: 0 + y: 11 + width: 190 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_status_label + text_align: CENTER + - label: + text: Waiting for Home Assistant + x: 24 + y: 88 + width: 752 + height: 28 + text_color: 15594487 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_task_label + - arc: + id: polly_pocket_progress_arc + x: 28 + y: 132 + width: 224 + height: 224 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 14 + arc_color: 2503746 + indicator: + arc_color: 6348442 + arc_width: 14 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 42 + y: 207 + width: 196 + height: 56 + text_color: 15594487 + text_font: montserrat_48 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_progress_label + text_align: CENTER + - label: + text: PRINT PROGRESS + x: 42 + y: 266 + width: 196 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - obj: + x: 282 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: NOZZLE / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_nozzle_label + - obj: + x: 538 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: BED / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_bed_label + - obj: + x: 282 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: REMAINING + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_remaining_label + - obj: + x: 538 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: LAYER + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_layers_label + - label: + text: Telemetry unavailable + x: 24 + y: 358 + width: 752 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_stage_label + - label: + text: FILAMENT / ACTIVE SLOT OUTLINED IN GREEN + x: 24 + y: 383 + width: 752 + height: 20 + text_color: 9544629 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + - obj: + id: polly_pocket_tray_0_card + x: 24 + y: 401 + width: 752 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: polly_pocket_tray_0_swatch + x: 1 + y: 1 + width: 748 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: EXT -- + x: 6 + y: 11 + width: 740 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: polly_pocket_tray_0_label + - button: + x: 24 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: < PREV + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.previous: {} + - button: + x: 688 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: NEXT > + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.next: {} + - label: + text: 2 / 5 SWIPE LEFT / RIGHT + x: 130 + y: 451 + width: 540 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - id: ds_turd_flinger_page + bg_color: 856859 + 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: ds_turd_flinger_refresh + widgets: + - label: + text: Ds-Turd-Flinger + x: 24 + y: 16 + width: 545 + height: 36 + text_color: 15594487 + text_font: montserrat_28 + long_mode: DOT + gesture_bubble: true + - label: + text: A1 / Upstairs + x: 24 + y: 53 + width: 545 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - obj: + x: 586 + y: 20 + width: 190 + height: 42 + bg_color: 1516078 + radius: 21 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: SYNCING + x: 0 + y: 11 + width: 190 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_status_label + text_align: CENTER + - label: + text: Waiting for Home Assistant + x: 24 + y: 88 + width: 752 + height: 28 + text_color: 15594487 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_task_label + - arc: + id: ds_turd_flinger_progress_arc + x: 28 + y: 132 + width: 224 + height: 224 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 14 + arc_color: 2503746 + indicator: + arc_color: 6348442 + arc_width: 14 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 42 + y: 207 + width: 196 + height: 56 + text_color: 15594487 + text_font: montserrat_48 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_progress_label + text_align: CENTER + - label: + text: PRINT PROGRESS + x: 42 + y: 266 + width: 196 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - obj: + x: 282 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: NOZZLE / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_nozzle_label + - obj: + x: 538 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: BED / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_bed_label + - obj: + x: 282 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: REMAINING + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_remaining_label + - obj: + x: 538 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: LAYER + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_layers_label + - label: + text: Telemetry unavailable + x: 24 + y: 358 + width: 752 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_stage_label + - label: + text: FILAMENT / ACTIVE SLOT OUTLINED IN GREEN + x: 24 + y: 383 + width: 752 + height: 20 + text_color: 9544629 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + - obj: + id: ds_turd_flinger_tray_0_card + x: 24 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: ds_turd_flinger_tray_0_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A1 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_tray_0_label + - obj: + id: ds_turd_flinger_tray_1_card + x: 214 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: ds_turd_flinger_tray_1_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A2 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_tray_1_label + - obj: + id: ds_turd_flinger_tray_2_card + x: 404 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: ds_turd_flinger_tray_2_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A3 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_tray_2_label + - obj: + id: ds_turd_flinger_tray_3_card + x: 594 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: ds_turd_flinger_tray_3_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A4 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: ds_turd_flinger_tray_3_label + - button: + x: 24 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: < PREV + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.previous: {} + - button: + x: 688 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: NEXT > + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.next: {} + - label: + text: 3 / 5 SWIPE LEFT / RIGHT + x: 130 + y: 451 + width: 540 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - id: jordyns_layer_slayer_page + bg_color: 856859 + 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: jordyns_layer_slayer_refresh + widgets: + - label: + text: Jordyns-Layer-Slayer + x: 24 + y: 16 + width: 545 + height: 36 + text_color: 15594487 + text_font: montserrat_28 + long_mode: DOT + gesture_bubble: true + - label: + text: A1 / Upstairs + x: 24 + y: 53 + width: 545 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - obj: + x: 586 + y: 20 + width: 190 + height: 42 + bg_color: 1516078 + radius: 21 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: SYNCING + x: 0 + y: 11 + width: 190 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_status_label + text_align: CENTER + - label: + text: Waiting for Home Assistant + x: 24 + y: 88 + width: 752 + height: 28 + text_color: 15594487 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_task_label + - arc: + id: jordyns_layer_slayer_progress_arc + x: 28 + y: 132 + width: 224 + height: 224 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 14 + arc_color: 2503746 + indicator: + arc_color: 6348442 + arc_width: 14 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 42 + y: 207 + width: 196 + height: 56 + text_color: 15594487 + text_font: montserrat_48 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_progress_label + text_align: CENTER + - label: + text: PRINT PROGRESS + x: 42 + y: 266 + width: 196 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - obj: + x: 282 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: NOZZLE / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_nozzle_label + - obj: + x: 538 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: BED / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_bed_label + - obj: + x: 282 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: REMAINING + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_remaining_label + - obj: + x: 538 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: LAYER + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_layers_label + - label: + text: Telemetry unavailable + x: 24 + y: 358 + width: 752 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_stage_label + - label: + text: FILAMENT / ACTIVE SLOT OUTLINED IN GREEN + x: 24 + y: 383 + width: 752 + height: 20 + text_color: 9544629 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + - obj: + id: jordyns_layer_slayer_tray_0_card + x: 24 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: jordyns_layer_slayer_tray_0_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A1 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_tray_0_label + - obj: + id: jordyns_layer_slayer_tray_1_card + x: 214 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: jordyns_layer_slayer_tray_1_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A2 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_tray_1_label + - obj: + id: jordyns_layer_slayer_tray_2_card + x: 404 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: jordyns_layer_slayer_tray_2_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A3 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_tray_2_label + - obj: + id: jordyns_layer_slayer_tray_3_card + x: 594 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: jordyns_layer_slayer_tray_3_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A4 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: jordyns_layer_slayer_tray_3_label + - button: + x: 24 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: < PREV + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.previous: {} + - button: + x: 688 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: NEXT > + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.next: {} + - label: + text: 4 / 5 SWIPE LEFT / RIGHT + x: 130 + y: 451 + width: 540 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - id: constipation_orion_page + bg_color: 856859 + 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: constipation_orion_refresh + widgets: + - label: + text: Constipation-Orion + x: 24 + y: 16 + width: 545 + height: 36 + text_color: 15594487 + text_font: montserrat_28 + long_mode: DOT + gesture_bubble: true + - label: + text: Dual nozzle / Office + x: 24 + y: 53 + width: 545 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - obj: + x: 586 + y: 20 + width: 190 + height: 42 + bg_color: 1516078 + radius: 21 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: SYNCING + x: 0 + y: 11 + width: 190 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_status_label + text_align: CENTER + - label: + text: Waiting for Home Assistant + x: 24 + y: 88 + width: 752 + height: 28 + text_color: 15594487 + text_font: montserrat_20 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_task_label + - arc: + id: constipation_orion_progress_arc + x: 28 + y: 132 + width: 224 + height: 224 + min_value: 0 + max_value: 100 + value: 0 + start_angle: 135 + end_angle: 45 + arc_width: 14 + arc_color: 2503746 + indicator: + arc_color: 6348442 + arc_width: 14 + adjustable: false + gesture_bubble: true + - label: + text: -- + x: 42 + y: 207 + width: 196 + height: 56 + text_color: 15594487 + text_font: montserrat_48 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_progress_label + text_align: CENTER + - label: + text: PRINT PROGRESS + x: 42 + y: 266 + width: 196 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER + - obj: + x: 282 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: NOZZLES L/R / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_18 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_nozzle_label + - obj: + x: 538 + y: 132 + width: 238 + height: 118 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: BED / TARGET + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 76 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_bed_label + - obj: + x: 282 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: REMAINING + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_remaining_label + - obj: + x: 538 + y: 262 + width: 238 + height: 86 + bg_color: 1516078 + radius: 16 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - label: + text: LAYER + x: 16 + y: 12 + width: 206 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + - label: + text: -- + x: 16 + y: 38 + width: 206 + height: 44 + text_color: 15594487 + text_font: montserrat_26 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_layers_label + - label: + text: Telemetry unavailable + x: 24 + y: 358 + width: 752 + height: 24 + text_color: 9544629 + text_font: montserrat_16 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_stage_label + - label: + text: FILAMENT / ACTIVE SLOT OUTLINED IN GREEN + x: 24 + y: 383 + width: 752 + height: 20 + text_color: 9544629 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + - obj: + id: constipation_orion_tray_0_card + x: 24 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: constipation_orion_tray_0_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A1 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_tray_0_label + - obj: + id: constipation_orion_tray_1_card + x: 214 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: constipation_orion_tray_1_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A2 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_tray_1_label + - obj: + id: constipation_orion_tray_2_card + x: 404 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: constipation_orion_tray_2_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A3 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_tray_2_label + - obj: + id: constipation_orion_tray_3_card + x: 594 + y: 401 + width: 182 + height: 32 + bg_color: 1516078 + radius: 6 + border_width: 1 + border_color: 2503746 + pad_all: 0 + scrollable: false + gesture_bubble: true + widgets: + - obj: + id: constipation_orion_tray_3_swatch + x: 1 + y: 1 + width: 178 + height: 5 + bg_color: 2503746 + radius: 2 + border_width: 0 + pad_all: 0 + scrollable: false + gesture_bubble: true + - label: + text: A4 -- + x: 6 + y: 11 + width: 170 + height: 20 + text_color: 15594487 + text_font: montserrat_12 + long_mode: DOT + gesture_bubble: true + id: constipation_orion_tray_3_label + - button: + x: 24 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: < PREV + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.previous: {} + - button: + x: 688 + y: 444 + width: 88 + height: 30 + bg_color: 1516078 + radius: 8 + border_width: 0 + pad_all: 0 + gesture_bubble: true + widgets: + - label: + text: NEXT > + align: CENTER + text_font: montserrat_12 + on_click: + - lvgl.page.next: {} + - label: + text: 5 / 5 SWIPE LEFT / RIGHT + x: 130 + y: 451 + width: 540 + height: 22 + text_color: 9544629 + text_font: montserrat_14 + long_mode: DOT + gesture_bubble: true + text_align: CENTER diff --git a/esphome/bambu-display/preview.py b/esphome/bambu-display/preview.py new file mode 100644 index 0000000..58537ea --- /dev/null +++ b/esphome/bambu-display/preview.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Render the real LVGL pages with demo telemetry using ESPHome's headless SDL host.""" + +import argparse +import json +from pathlib import Path +import subprocess +import tempfile + +from PIL import Image +import yaml + +from generate import Dumper, HERE, Lambda, generate + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, default=HERE / 'previews') + args = parser.parse_args() + printers = yaml.safe_load((HERE / 'printers.yaml').read_text())['printers'] + package = generate(printers) + 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, 'left_nozzle': 219, 'left_target': 220, + 'right_nozzle': 37, 'right_target': 0} + demo_text = {'status': 'running', 'task': 'Workshop organizer - plate 2', + 'stage': 'printing', 'online': 'on', 'print_error': 'off', 'hms_error': 'off', + 'remaining_unit': 'h', 'nozzle_unit': '°C', 'bed_unit': '°C'} + colors = ['#60DE9AFF', '#F4C96BFF', '#74A9E8FF', '#F8F4ECFF', + '#DF8BBFFF', '#FA936BFF', '#8E79CAFF', '#4CC9C1FF'] + for printer in printers: + prefix = printer['id'] + '_' + for section in ('sensor', 'text_sensor'): + for entry in package[section]: + if not entry['id'].startswith(prefix): + continue + key = entry['id'][len(prefix):] + if section == 'sensor': + value = str(demo_values[key]) + else: + if key.startswith('tray_'): + _, index, attribute = key.split('_') + text = {'type': 'PLA', 'color': colors[int(index) % len(colors)], + 'active': 'on' if index == '0' else 'off'}[attribute] + else: + text = demo_text[key] + value = 'std::string(' + json.dumps(text, ensure_ascii=False) + ')' + entry.clear() + entry.update({'platform': 'template', 'id': prefix + key, 'internal': True, + 'lambda': Lambda('return ' + value + ';'), 'update_interval': '60s'}) + for script in package['script']: + code = script['then'][0]['lambda'] + script['then'][0]['lambda'] = Lambda(code.replace( + 'id(ha_api).is_connected_with_state_subscription()', 'true')) + + args.output.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='bambu-lvgl-preview-') as work: + root = Path(work) + # Inject real LVGL pointer samples; test bubbling through cards/buttons + # without a GUI window or a physical touchscreen. + touch_header = root / 'preview_touch.h' + touch_header.write_text('''#pragma once +#include +namespace bambu_preview { +inline int x = 0, y = 0; +inline bool pressed = false; +inline void setup() { + auto *input = lv_indev_create(); + lv_indev_set_type(input, LV_INDEV_TYPE_POINTER); + lv_indev_set_display(input, lv_display_get_default()); + lv_indev_set_read_cb(input, [](lv_indev_t *, lv_indev_data_t *data) { + data->point.x = x; + data->point.y = y; + data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + }); +} +} +''') + steps = [{'component.update': entry['id']} + for section in ('sensor', 'text_sensor') for entry in package[section]] + steps += [{'delay': '1s'}] + for printer in printers: + steps += [{'lvgl.page.show': printer['id'] + '_page'}, + {'script.execute': printer['id'] + '_refresh'}, {'delay': '300ms'}, + {'snapshot.take': {'id': 'panel_display', 'filename': printer['id'] + '.bmp'}}] + steps += [{'lvgl.page.show': printers[0]['id'] + '_page'}, {'delay': '250ms'}, + {'lambda': Lambda('bambu_preview::setup();')}] + if len(printers) > 1: + # First -> second -> first -> last -> first, then a swipe starting + # on NEXT must advance exactly once even when released over PREV. + gestures = [(650, 180, 180, printers[1]['id']), + (180, 650, 180, printers[0]['id']), + (180, 650, 180, printers[-1]['id']), + (650, 180, 180, printers[0]['id']), + (730, 60, 460, printers[1]['id'])] + for start, end, y, expected in gestures: + steps += [ + {'lambda': Lambda(f'bambu_preview::x = {start}; bambu_preview::y = {y}; bambu_preview::pressed = true;')}, + {'delay': '70ms'}, + {'lambda': Lambda(f'bambu_preview::x = {end};')}, {'delay': '70ms'}, + {'lambda': Lambda('bambu_preview::pressed = false;')}, {'delay': '350ms'}, + {'lambda': Lambda(f'if (!id({expected}_page).is_showing()) {{ ESP_LOGE("preview", "Swipe navigation failed"); exit(2); }}')}, + ] + steps += [{'lambda': Lambda('exit(0);')}] + config = {'esphome': {'name': 'bambu-display-preview', + 'includes': [str(HERE / 'dashboard.h'), str(touch_header)], + 'on_boot': {'priority': -100, 'then': steps}}, + 'host': {}, 'logger': {'level': 'WARN'}, + 'display': [{'platform': 'sdl', 'id': 'panel_display', 'dimensions': '800x480', + 'headless': True, 'auto_clear_enabled': False, 'update_interval': 'never'}], + 'lvgl': {'id': 'dashboard', 'displays': ['panel_display'], 'buffer_size': '100%', + 'color_depth': 16, 'bg_color': 0x0D131B, 'text_color': 0xEDF3F7, + 'default_font': 'montserrat_18', **package.pop('lvgl')}, **package} + filename = root / 'preview.yaml' + filename.write_text(yaml.dump(config, Dumper=Dumper, sort_keys=False, allow_unicode=True)) + subprocess.run(['esphome', 'compile', str(filename)], check=True) + binary = root / '.esphome/build/bambu-display-preview/.pioenvs/bambu-display-preview/program' + subprocess.run([str(binary)], check=True, timeout=30, cwd=root) + for printer in printers: + bmp = root / '.esphome/snapshots/bambu-display-preview' / (printer['id'] + '.bmp') + with Image.open(bmp) as image: + image.save(args.output / (printer['id'] + '.png')) + print(f'Saved {len(printers)} demo screenshots to {args.output}') + print('Passed LVGL pointer swipe tests, including wraparound and button gesture consumption.') + + +if __name__ == '__main__': + main() diff --git a/esphome/bambu-display/previews/constipation_orion.png b/esphome/bambu-display/previews/constipation_orion.png new file mode 100644 index 0000000..8814633 Binary files /dev/null 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 new file mode 100644 index 0000000..961c5aa Binary files /dev/null and b/esphome/bambu-display/previews/ds_turd_flinger.png differ diff --git a/esphome/bambu-display/previews/jordyns_layer_slayer.png b/esphome/bambu-display/previews/jordyns_layer_slayer.png new file mode 100644 index 0000000..4d38a1a Binary files /dev/null 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 new file mode 100644 index 0000000..f15fa09 Binary files /dev/null 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 new file mode 100644 index 0000000..ff2b483 Binary files /dev/null and b/esphome/bambu-display/previews/prints_charming.png differ diff --git a/esphome/bambu-display/printers.yaml b/esphome/bambu-display/printers.yaml new file mode 100644 index 0000000..894ab0e --- /dev/null +++ b/esphome/bambu-display/printers.yaml @@ -0,0 +1,46 @@ +# Order here is the swipe order. Add/remove entries and rerun generate.py. +# entity_prefix is the HA entity ID between the domain and the metric suffix. +# Optional entities: {metric: sensor.custom_entity} overrides individual mappings. +printers: + - id: prints_charming + name: PrintsCharming + subtitle: P1S / Office + entity_prefix: p1s_01p09c470102673 + filament_banks: + - label: A + entity_prefix: p1s_01p09c470102673_ams_1 + - label: B + entity_prefix: p1s_01p09c470102673_ams_2 + + - id: polly_pocket + name: Polly-Pocket + subtitle: A1 mini / Office + entity_prefix: a1mini_0309ca580403653 + external_spools: + - label: EXT + entity: sensor.a1mini_0309ca580403653_externalspool_external_spool + + - id: ds_turd_flinger + name: Ds-Turd-Flinger + subtitle: A1 / Upstairs + entity_prefix: a1_03919d540806387 + filament_banks: + - label: A + entity_prefix: a1_03919d540806387_ams_1 + + - id: jordyns_layer_slayer + name: Jordyns-Layer-Slayer + subtitle: A1 / Upstairs + entity_prefix: jordyns_layer_slayer + filament_banks: + - label: A + entity_prefix: jordyns_layer_slayer_ams + + - id: constipation_orion + name: Constipation-Orion + subtitle: Dual nozzle / Office + entity_prefix: constipation_orion + dual_nozzle: true + filament_banks: + - label: A + entity_prefix: constipation_orion_ams_1 diff --git a/esphome/bambu-display/test_dashboard.cpp b/esphome/bambu-display/test_dashboard.cpp new file mode 100644 index 0000000..80dc8ce --- /dev/null +++ b/esphome/bambu-display/test_dashboard.cpp @@ -0,0 +1,54 @@ +#include "dashboard.h" +#include + +int main() { + using namespace bambu_display; + assert(remaining(1.75f, "h") == "1h 45m"); + assert(remaining(105.0f, "min") == "1h 45m"); + assert(remaining(6300.0f, "s") == "1h 45m"); + assert(remaining(0.15f, "h") == "0h 09m"); + assert(remaining(NAN, "h") == "--"); + assert(remaining(-1, "min") == "--"); + assert(remaining(10, "") == "--"); + assert(remaining(INFINITY, "h") == "--"); + assert(number(NAN) == "--"); + assert(temperature(219, 220, "°C") == "219 / 220 °C"); + assert(filament_color("#60DE9AFF") == GREEN); + assert(filament_color("60de9a") == GREEN); + assert(filament_color("#60DE9AZZ") == TRACK); + assert(filament_color("unknown") == TRACK); + assert(gauge_value(NAN, 100) == 0); + assert(gauge_value(110, 100) == 100); + assert(gauge_value(-10, 100) == 0); + + Snapshot data; + data.online = "on"; + data.status = "running"; + assert(!data.live()); + assert(data.status_text() == "HA OFFLINE"); + data.connected = true; + assert(data.live()); + assert(data.status_text() == "PRINTING"); + data.online = "off"; + assert(!data.live()); + assert(data.status_text() == "PRINTER OFFLINE"); + data.online = "unavailable"; + assert(data.status_text() == "SYNCING"); + data.online = "on"; + data.status = "unknown"; + assert(!data.live()); + data.status = "pause"; + assert(data.status_text() == "PAUSED"); + assert(data.status_color() == AMBER); + data.print_error = "on"; + assert(data.status_text() == "PRINT ERROR"); + assert(data.status_color() == RED); + data.print_error = "off"; + data.hms_error = "True"; + assert(data.status_text() == "HMS ALERT"); + assert(data.status_color() == RED); + data.hms_error = "off"; + data.status = "finish"; + assert(data.status_text() == "COMPLETE"); + assert(data.status_color() == GREEN); +} diff --git a/esphome/bambu-display/test_generate.py b/esphome/bambu-display/test_generate.py new file mode 100644 index 0000000..822b82b --- /dev/null +++ b/esphome/bambu-display/test_generate.py @@ -0,0 +1,67 @@ +"""Run with: python -m unittest discover -s esphome/bambu-display -v.""" + +import copy +import unittest + +import yaml + +from generate import HERE, generate, validate_printers + + +class FleetTests(unittest.TestCase): + def setUp(self): + self.printers = yaml.safe_load((HERE / 'printers.yaml').read_text())['printers'] + + def test_all_subscriptions_match_repository_entities(self): + known = {line.split(' | ')[0] for line in + (HERE.parent / 'ha-entities.txt').read_text().splitlines()} + package = generate(self.printers) + subscribed = {entry['entity_id'] for domain in ('sensor', 'text_sensor') + for entry in package[domain]} + self.assertFalse(subscribed - known) + + def test_seven_printers_have_independent_pages_and_metrics(self): + for index in (6, 7): + 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(package['lvgl']['pages']), 7) + self.assertEqual(len(package['script']), 7) + subscriptions = {entry['id']: entry['entity_id'] for entry in package['sensor']} + self.assertEqual(subscriptions['printer_7_progress'], 'sensor.new_printer_7_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_overrides_apply_to_values_and_unit_subscriptions(self): + self.printers[0]['entities'] = {'time_left': 'sensor.custom_remaining'} + package = generate(self.printers) + for domain, key in [('sensor', 'time_left'), ('text_sensor', 'remaining_unit')]: + entry = next(row for row in package[domain] if row['id'] == f'prints_charming_{key}') + self.assertEqual(entry['entity_id'], 'sensor.custom_remaining') + + def test_reject_empty_duplicate_and_malformed_printer_ids(self): + for invalid in ([], self.printers + [self.printers[0]], + [{**self.printers[0], 'id': 'invalid-id'}]): + with self.subTest(invalid=invalid), self.assertRaises(ValueError): + validate_printers(invalid) + + def test_reject_overflowing_filament_strip(self): + self.printers[0]['external_spools'] = [{'label': 'EXT', 'entity': 'sensor.external_spool'}] + with self.assertRaisesRegex(ValueError, 'eight slots'): + generate(self.printers) + + def test_progress_and_dual_nozzle_widgets_fit_the_panel(self): + package = generate(self.printers) + for page in package['lvgl']['pages']: + for widget in page['widgets']: + props = next(iter(widget.values())) + self.assertGreaterEqual(props.get('x', 0), 0) + self.assertGreaterEqual(props.get('y', 0), 0) + self.assertLessEqual(props.get('x', 0) + props.get('width', 0), 800) + self.assertLessEqual(props.get('y', 0) + props.get('height', 0), 480) + + +if __name__ == '__main__': + unittest.main() diff --git a/esphome/bambu-printer-display.yaml b/esphome/bambu-printer-display.yaml new file mode 100644 index 0000000..b34030b --- /dev/null +++ b/esphome/bambu-printer-display.yaml @@ -0,0 +1,114 @@ +# Sunton ESP32-8048S043C: 4.3" 800x480 RGB LCD + GT911 capacitive touch. +# Edit bambu-display/printers.yaml, then run its generate.py to change the fleet. +substitutions: + device_name: bambu-printer-display + friendly_name: Bambu Printer Display + +esphome: + name: ${device_name} + friendly_name: ${friendly_name} + min_version: 2026.9.0 + includes: + - bambu-display/dashboard.h + +esp32: + board: esp32-s3-devkitc-1 + flash_size: 16MB + flash_mode: dio + cpu_frequency: 240MHz + framework: + type: esp-idf + sdkconfig_options: + CONFIG_ESP32S3_DATA_CACHE_64KB: "y" + CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" + CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y" + CONFIG_SPIRAM_RODATA: "y" + +psram: + mode: octal + speed: 80MHz + +logger: + hardware_uart: UART0 # GPIO19/20 are the touch bus, not native USB. + +api: + id: ha_api + encryption: + key: !secret api_encryption_key + +ota: + - platform: esphome + encryption: + key: !secret api_encryption_key + +wifi: + ssid: !secret wifi_iot_ssid + password: !secret wifi_password + +i2c: + id: touch_bus + sda: GPIO19 + scl: GPIO20 + scan: true + +display: + - platform: mipi_rgb + id: panel_display + model: RPI + dimensions: 800x480 + color_order: RGB + byte_order: little_endian + de_pin: GPIO40 + hsync_pin: GPIO39 + vsync_pin: GPIO41 + pclk_pin: GPIO42 + pclk_inverted: true + pclk_frequency: 16MHz + hsync_front_porch: 8 + hsync_pulse_width: 4 + hsync_back_porch: 8 + vsync_front_porch: 8 + vsync_pulse_width: 4 + vsync_back_porch: 8 + data_pins: + red: [GPIO45, GPIO48, GPIO47, GPIO21, GPIO14] + green: [GPIO5, GPIO6, GPIO7, GPIO15, GPIO16, GPIO4] + blue: [GPIO8, GPIO3, GPIO46, GPIO9, GPIO1] + auto_clear_enabled: false + update_interval: never + +touchscreen: + - platform: gt911 + id: panel_touch + display: panel_display + i2c_id: touch_bus + address: 0x5D + # Polling avoids the GT911 INT pin's boot/address-selection behaviour. + update_interval: 20ms + +output: + - platform: ledc + id: backlight_output + pin: GPIO2 + +light: + - platform: monochromatic + id: panel_backlight + name: Backlight + output: backlight_output + restore_mode: ALWAYS_ON + default_transition_length: 0s + +packages: + fleet: !include bambu-display/generated.yaml + +lvgl: + id: dashboard + displays: [panel_display] + touchscreens: [panel_touch] + buffer_size: 12% + color_depth: 16 + byte_order: little_endian + bg_color: 0x0D131B + text_color: 0xEDF3F7 + default_font: montserrat_18