486 lines
26 KiB
Python
486 lines
26 KiB
Python
#!/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',
|
|
}
|
|
EXTRA_FANS = {'aux_fan': 'aux_fan_speed', 'exhaust_fan': 'chamber_fan_speed'}
|
|
TEXT = {'status': 'print_status', 'task': 'task_name', 'stage': 'current_stage'}
|
|
BINARY = {'online': 'online', 'print_error': 'print_error', 'hms_error': 'hms_errors'}
|
|
ELEGOO_COMMON = {'progress': 'percent_complete', 'time_left': 'remaining_print_time',
|
|
'layer': 'current_layer', 'total_layers': 'total_layers',
|
|
'total_time': 'total_print_time'}
|
|
ELEGOO_FDM = {'nozzle': 'nozzle_temperature', 'bed': 'bed_temperature',
|
|
'elapsed': 'current_print_time',
|
|
'nozzle_target': ('number', 'target_nozzle_temp'),
|
|
'bed_target': ('number', 'target_bed_temp'), 'fan': 'model_fan_speed',
|
|
'aux_fan': 'auxiliary_fan_speed', 'exhaust_fan': 'enclosure_fan_speed'}
|
|
ELEGOO_RESIN = {'uv': 'uv_led_temp', 'elapsed': 'current_print_time'}
|
|
BG, CARD, TRACK, TEXT_COLOR, MUTED, GREEN = (
|
|
0x000000, 0x000000, 0x19232B, 0xEEEEEE, 0xAEB3B7, 0x00FF00)
|
|
RING_BACKING = 0x6E767E
|
|
ORANGE, CYAN = 0xFFAA00, 0x00E5E5
|
|
|
|
|
|
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)
|
|
profile = printer.get('profile', 'bambu')
|
|
if profile not in ('bambu', 'elegoo_fdm', 'elegoo_resin'):
|
|
raise ValueError(f'{pid}: unsupported profile: {profile}')
|
|
if profile != 'bambu' and (printer.get('dual_nozzle') or trays_for(printer) or printer.get('extra_fans')):
|
|
raise ValueError(f'{pid}: dual_nozzle, filament slots and extra_fans require the bambu profile')
|
|
if len(trays_for(printer)) > 8:
|
|
raise ValueError(f'{pid}: the filament strip fits up to eight slots')
|
|
if 'dual_nozzle' in printer and not isinstance(printer['dual_nozzle'], bool):
|
|
raise ValueError(f'{pid}: dual_nozzle must be true or false')
|
|
if any(key not in EXTRA_FANS for key in printer.get('extra_fans', [])):
|
|
raise ValueError(f'{pid}: extra_fans supports aux_fan and exhaust_fan')
|
|
for value in printer.get('entities', {}).values():
|
|
if not isinstance(value, str) or not re.fullmatch(r'(sensor|binary_sensor|number)\.[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 gauge(pid, key, title, x, y, color, maximum):
|
|
return [
|
|
{'arc': {'id': f'{pid}_{key}_arc', 'x': x + 8, 'y': y, 'width': 154, 'height': 154,
|
|
'min_value': 0, 'max_value': maximum, 'value': 0,
|
|
'start_angle': 135, 'end_angle': 45, 'arc_width': 13,
|
|
'arc_color': TRACK, 'indicator': {'arc_color': color, 'arc_width': 13},
|
|
'adjustable': False, 'gesture_bubble': True}},
|
|
label('--', x + 14, y + 47, 142, 32, id=f'{pid}_{key}_label', text_align='CENTER'),
|
|
label('--', x + 14, y + 86, 142, 16, id=f'{pid}_{key}_detail', text_align='CENTER', text_color=MUTED),
|
|
label(title, x, y + 137, 170, 18, text_align='CENTER', text_color=color),
|
|
]
|
|
|
|
|
|
def fdm_gauges(printer):
|
|
pid = printer['id']
|
|
widgets = gauge(pid, 'nozzle', 'Nozzle L' if printer.get('dual_nozzle') else 'Nozzle',
|
|
202, 62, ORANGE, 300)
|
|
widgets += gauge(pid, 'bed', 'Bed', 388, 62, CYAN, 120)
|
|
widgets += gauge(pid, 'fan', 'Part', 16, 229, 0xEEEEEE, 100)
|
|
widgets += gauge(pid, 'aux', 'Nozzle R' if printer.get('dual_nozzle') else 'Aux',
|
|
202, 229, ORANGE if printer.get('dual_nozzle') else 0xEEEEEE,
|
|
300 if printer.get('dual_nozzle') else 100)
|
|
widgets += gauge(pid, 'exhaust', 'Exhaust', 388, 229, GREEN, 100)
|
|
return widgets
|
|
|
|
|
|
def page_for(printer, display_index, total):
|
|
pid = printer['id']
|
|
resin = printer.get('profile') == 'elegoo_resin'
|
|
widgets = [
|
|
{'bar': {'id': f'{pid}_top_progress', 'x': 0, 'y': 0, 'width': 800, 'height': 5,
|
|
'min_value': 0, 'max_value': 100, 'value': 0, 'bg_color': TRACK,
|
|
'indicator': {'bg_color': GREEN}, 'gesture_bubble': True}},
|
|
label(printer['name'], 16, 17, 485, 26, text_color=GREEN),
|
|
label(f'{display_index} / {total}', 520, 24, 70, 14, text_color=MUTED, text_align='CENTER'),
|
|
label('SYNCING', 598, 21, 186, 16, id=f'{pid}_status_label', text_align='RIGHT', text_color=MUTED,
|
|
clickable=True, on_click=[{'lvgl.widget.show': f'{pid}_health_panel'}]),
|
|
{'obj': {'x': 578, 'y': 64, 'width': 1, 'height': 362, 'bg_color': TRACK,
|
|
'border_width': 0, 'pad_all': 0, 'scrollable': False, 'gesture_bubble': True}},
|
|
]
|
|
widgets += gauge(pid, 'progress', 'Progress', 16, 62, GREEN, 100)
|
|
if resin:
|
|
widgets += gauge(pid, 'uv', 'UV LED', 202, 62, ORANGE, 100)
|
|
widgets += gauge(pid, 'layer', 'Layer', 388, 62, CYAN, 100)
|
|
widgets += [label('FINISH / TIME REMAINING', 16, 260, 544, 18,
|
|
text_align='CENTER', text_color=MUTED),
|
|
label('--', 16, 354, 544, 20, id=f'{pid}_elapsed_label',
|
|
text_align='CENTER', text_color=MUTED)]
|
|
else:
|
|
widgets += fdm_gauges(printer)
|
|
widgets += [
|
|
label('--', 16, 300 if resin else 401, 544, 26, id=f'{pid}_remaining_label', text_color=GREEN, text_align='CENTER'),
|
|
label('Telemetry unavailable', 16, 430, 544, 12, id=f'{pid}_stage_label', text_color=MUTED,
|
|
text_align='CENTER', long_mode='SCROLL_CIRCULAR', clickable=True,
|
|
on_click=[{'lvgl.widget.show': f'{pid}_health_panel'}]),
|
|
label('--', 594, 397, 190, 16, id=f'{pid}_layers_label', text_color=MUTED, text_align='CENTER'),
|
|
]
|
|
trays = trays_for(printer)
|
|
for i, tray in enumerate(trays):
|
|
bank = i // 4
|
|
slot = i % 4
|
|
if slot == 0:
|
|
title = 'AMS ' + tray['label'][:-1] if tray['label'][-1:].isdigit() else 'EXTERNAL SPOOL'
|
|
widgets.append(label(title, 594, 70 + bank * 145, 190, 20,
|
|
text_align='CENTER', text_color=MUTED))
|
|
widgets.append({'obj': {
|
|
'id': f'{pid}_tray_{i}_card', 'x': 594 + 48 * slot, 'y': 102 + bank * 145,
|
|
'width': 44, 'height': 82, 'bg_color': BG, 'radius': 5,
|
|
'border_width': 1, 'border_color': TRACK, 'pad_all': 0,
|
|
'scrollable': False, 'gesture_bubble': True,
|
|
'widgets': [
|
|
{'obj': {'id': f'{pid}_tray_{i}_swatch', 'x': 3, 'y': 3, 'width': 36,
|
|
'height': 40, 'bg_color': TRACK, 'radius': 4, 'border_width': 0,
|
|
'pad_all': 0, 'scrollable': False, 'gesture_bubble': True}},
|
|
label(f'{tray["label"]}\n--', 1, 46, 40, 12, height=34,
|
|
id=f'{pid}_tray_{i}_label', text_align='CENTER'),
|
|
]}})
|
|
if not trays:
|
|
widgets.append(label('ELEGOO' if printer.get('profile', '').startswith('elegoo_') else 'No AMS', 594, 150, 190, 20,
|
|
text_color=MUTED, text_align='CENTER'))
|
|
widgets.append(label(printer['subtitle'], 594, 185, 190, 16,
|
|
text_color=MUTED, text_align='CENTER'))
|
|
for direction, x, text in (('previous', 16, '<'), ('next', 720, '>')):
|
|
widgets.append({'button': {'x': x, 'y': 446, 'width': 64, 'height': 30,
|
|
'bg_color': TRACK, 'radius': 8, 'border_width': 0,
|
|
'pad_all': 0, 'gesture_bubble': True,
|
|
'widgets': [{'label': {'text': text, 'align': 'CENTER',
|
|
'text_color': TEXT_COLOR, 'text_font': 'montserrat_18'}}],
|
|
'on_click': [{f'lvgl.page.{direction}': {}}]}})
|
|
widgets.append(label('Waiting for Home Assistant', 94, 449, 612, 14,
|
|
id=f'{pid}_task_label', text_align='CENTER'))
|
|
widgets.append({'obj': {
|
|
'id': f'{pid}_health_panel', 'x': 24, 'y': 62, 'width': 752, 'height': 366,
|
|
'hidden': True, 'bg_color': BG, 'bg_opa': 'COVER', 'border_width': 2,
|
|
'border_color': MUTED, 'radius': 10, 'pad_all': 0, 'scrollable': False,
|
|
'gesture_bubble': False,
|
|
'widgets': [
|
|
label('Printer health / ' + printer['name'], 16, 14, 716, 20),
|
|
{'obj': {'x': 12, 'y': 50, 'width': 724, 'height': 246,
|
|
'bg_color': BG, 'border_width': 0, 'pad_all': 0,
|
|
'scroll_dir': 'VER', 'scrollbar_mode': 'AUTO', 'gesture_bubble': False,
|
|
'widgets': [label('No current health notifications.', 0, 0, 700, 18,
|
|
id=f'{pid}_health_details', long_mode='WRAP', height='SIZE_CONTENT')]}},
|
|
{'button': {'x': 624, 'y': 310, 'width': 108, 'height': 40, 'bg_color': TRACK,
|
|
'radius': 6, 'widgets': [{'label': {'text': 'CLOSE', 'align': 'CENTER'}}],
|
|
'on_click': [{'lvgl.widget.hide': f'{pid}_health_panel'}]}},
|
|
]}})
|
|
return {'id': f'{pid}_page', 'bg_color': BG, 'bg_opa': 'COVER',
|
|
'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 snapshot_lines(printer, expressions, trays):
|
|
profile = printer.get('profile', 'bambu')
|
|
elegoo = profile.startswith('elegoo_')
|
|
resin = profile == 'elegoo_resin'
|
|
lines = ['bambu_display::Snapshot data;',
|
|
'data.connected = id(ha_api).is_connected_with_state_subscription();']
|
|
for key, expr in expressions.items():
|
|
if not key.startswith(('tray_', 'hms_1_', 'hms_2_', 'hms_3_')):
|
|
lines.append(f'data.{key} = {expr};')
|
|
if elegoo:
|
|
lines.append('data.elegoo = true;')
|
|
else:
|
|
for number in range(1, 4):
|
|
lines.append('data.health_notices.push_back({' + ', '.join(
|
|
expressions[f'hms_{number}_{key}'] for key in ('code', 'message', 'severity')) + '});')
|
|
if resin:
|
|
lines.append('data.resin = true;')
|
|
if printer.get('dual_nozzle'):
|
|
lines.append('data.dual_nozzle = true;')
|
|
lines += [
|
|
'const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit);',
|
|
'const auto clock = id(ha_time).now();',
|
|
'if (clock.is_valid() && seconds_left >= 0)',
|
|
' data.finish_time = esphome::ESPTime::from_epoch_local(clock.timestamp + seconds_left).strftime("%H:%M");',
|
|
]
|
|
for i, tray in enumerate(trays):
|
|
lines.append('data.trays.push_back({' + ', '.join([
|
|
json.dumps(tray['label']), expressions[f'tray_{i}_type'],
|
|
expressions[f'tray_{i}_color'], expressions[f'tray_{i}_active']]) + '});')
|
|
return lines
|
|
|
|
|
|
def overview_slots(count):
|
|
"""Place one steam gauge per printer under the page title."""
|
|
columns = 4 if count > 4 else max(count, 1)
|
|
rows = (count + columns - 1) // columns
|
|
top, bottom = 46, 432
|
|
row_h = (bottom - top) // rows
|
|
arc = min(154, (800 // columns) - 24, row_h - 48)
|
|
col_w = 800 // columns
|
|
slots = []
|
|
for index in range(count):
|
|
row, col = divmod(index, columns)
|
|
in_row = min(columns, count - row * columns)
|
|
x0 = (800 - in_row * col_w) // 2
|
|
gauge_w = arc + 16
|
|
slots.append((x0 + col * col_w + (col_w - gauge_w) // 2,
|
|
top + row * row_h, arc))
|
|
return slots
|
|
|
|
|
|
def overview_gauge(printer, x, y, arc):
|
|
pid = printer['id']
|
|
scale = arc / 154
|
|
value_font = 32 if arc >= 130 else 18
|
|
open_page = [{'lvgl.page.show': f'{pid}_page'}]
|
|
inner = max(10, int(round(13 * scale)))
|
|
inner_x = x + int(round(8 * scale))
|
|
number_y = y + int(round(arc * 0.30))
|
|
pad = 8
|
|
return [
|
|
{'arc': {'id': f'{pid}_overview_ring', 'x': inner_x - pad // 2, 'y': y - pad // 2,
|
|
'width': arc + pad, 'height': arc + pad, 'min_value': 0, 'max_value': 100,
|
|
'value': 100, 'start_angle': 135, 'end_angle': 45,
|
|
'arc_width': inner + pad, 'arc_color': RING_BACKING,
|
|
'indicator': {'arc_color': RING_BACKING, 'arc_width': inner + pad},
|
|
'adjustable': False, 'hidden': True, 'gesture_bubble': True, 'clickable': True,
|
|
'on_click': open_page}},
|
|
{'arc': {'id': f'{pid}_overview_arc', 'x': inner_x, 'y': y,
|
|
'width': arc, 'height': arc, 'min_value': 0, 'max_value': 100, 'value': 0,
|
|
'start_angle': 135, 'end_angle': 45, 'arc_width': inner, 'arc_color': TRACK,
|
|
'indicator': {'arc_color': GREEN, 'arc_width': inner}, 'adjustable': False,
|
|
'gesture_bubble': True, 'clickable': True, 'on_click': open_page}},
|
|
label('--', inner_x, number_y, arc, value_font, id=f'{pid}_overview_value',
|
|
text_align='CENTER', text_color=0xFFFFFF, clickable=True, on_click=open_page),
|
|
label('%', inner_x, number_y + (34 if value_font >= 28 else 20), arc, 16,
|
|
id=f'{pid}_overview_percent', text_align='CENTER', text_color=0xFFFFFF,
|
|
clickable=True, on_click=open_page),
|
|
label(printer['name'], x, y + arc + 2, arc + 16, 14, text_align='CENTER',
|
|
text_color=TEXT_COLOR, clickable=True, on_click=open_page),
|
|
label('--', x, y + arc + 22, arc + 16, 14, id=f'{pid}_overview_finish',
|
|
text_align='CENTER', text_color=TEXT_COLOR, clickable=True, on_click=open_page),
|
|
]
|
|
|
|
|
|
def overview_page(printers):
|
|
widgets = [
|
|
label('Print Status Overview', 16, 10, 520, 22, text_color=GREEN),
|
|
label('--:--', 548, 12, 236, 18, id='fleet_clock', text_align='RIGHT', text_color=TEXT_COLOR),
|
|
]
|
|
for printer, (x, y, arc) in zip(printers, overview_slots(len(printers))):
|
|
widgets += overview_gauge(printer, x, y, arc)
|
|
for direction, x, text in (('previous', 16, '<'), ('next', 720, '>')):
|
|
widgets.append({'button': {'x': x, 'y': 446, 'width': 64, 'height': 30,
|
|
'bg_color': TRACK, 'radius': 8, 'border_width': 0,
|
|
'pad_all': 0, 'gesture_bubble': True,
|
|
'widgets': [{'label': {'text': text, 'align': 'CENTER',
|
|
'text_color': TEXT_COLOR, 'text_font': 'montserrat_18'}}],
|
|
'on_click': [{f'lvgl.page.{direction}': {}}]}})
|
|
widgets.append(label(f'1 / {len(printers) + 1}', 94, 449, 612, 14,
|
|
text_align='CENTER', text_color=MUTED))
|
|
return {'id': 'fleet_page', 'bg_color': BG, 'bg_opa': 'COVER',
|
|
'pad_all': 0, 'scrollable': False, 'scrollbar_mode': 'OFF',
|
|
'on_swipe_left': [
|
|
{'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')},
|
|
{'lvgl.page.next': {'animation': 'MOVE_LEFT', 'time': '180ms'}}],
|
|
'on_swipe_right': [
|
|
{'lambda': Lambda('if (auto *input = lv_indev_active()) lv_indev_wait_release(input);')},
|
|
{'lvgl.page.previous': {'animation': 'MOVE_RIGHT', 'time': '180ms'}}],
|
|
'on_load': [{'script.execute': 'fleet_refresh'}], 'widgets': widgets}
|
|
|
|
|
|
def fleet_refresh_lines(snapshots):
|
|
lines = []
|
|
for pid, snapshot in snapshots:
|
|
lines.append('{')
|
|
lines.extend(snapshot)
|
|
lines.append('bambu_display::render_overview(data, {' + ', '.join([
|
|
f'id({pid}_overview_arc)', f'id({pid}_overview_value)',
|
|
f'id({pid}_overview_finish)', f'id({pid}_overview_ring)']) + '});')
|
|
lines.append('}')
|
|
lines += [
|
|
'{',
|
|
'const auto clock = id(ha_time).now();',
|
|
'std::string clock_text = "--:--";',
|
|
'if (clock.is_valid()) {',
|
|
' clock_text = esphome::ESPTime::from_epoch_local(clock.timestamp).strftime("%I:%M %p");',
|
|
' if (!clock_text.empty() && clock_text[0] == \'0\') clock_text.erase(0, 1);',
|
|
'}',
|
|
'bambu_display::set_text(id(fleet_clock), clock_text);',
|
|
'}',
|
|
]
|
|
return lines
|
|
|
|
|
|
def color_test_page():
|
|
widgets = [label('DISPLAY COLOR TEST', 16, 18, 768, 28),
|
|
label('Each block must match its label. Swipe to return to the printers.',
|
|
16, 62, 768, 18)]
|
|
colors = [('RED', 0xFF0000), ('GREEN', 0x00FF00), ('BLUE', 0x0000FF),
|
|
('WHITE', 0xFFFFFF), ('CYAN', 0x00FFFF), ('MAGENTA', 0xFF00FF)]
|
|
for i, (name, color) in enumerate(colors):
|
|
widgets += [
|
|
{'obj': {'x': 16 + i * 129, 'y': 118, 'width': 122, 'height': 200,
|
|
'bg_color': color, 'bg_opa': 'COVER', 'border_width': 0,
|
|
'scrollable': False, 'gesture_bubble': True}},
|
|
label(name, 16 + i * 129, 332, 122, 18, text_align='CENTER'),
|
|
]
|
|
return {'id': 'color_test_page', 'skip': True, 'bg_color': BG, 'bg_opa': 'COVER',
|
|
'pad_all': 0, 'scrollable': False,
|
|
'on_swipe_left': [{'lvgl.page.next': {}}],
|
|
'on_swipe_right': [{'lvgl.page.previous': {}}], 'widgets': widgets}
|
|
|
|
|
|
def generate(printers):
|
|
validate_printers(printers)
|
|
package = {'sensor': [], 'text_sensor': [], 'script': [],
|
|
'interval': [{'interval': '1s', 'then': []}], 'lvgl': {'pages': []}}
|
|
snapshots = []
|
|
page_total = len(printers) + 1
|
|
for index, printer in enumerate(printers):
|
|
pid = printer['id']
|
|
profile = printer.get('profile', 'bambu')
|
|
elegoo = profile.startswith('elegoo_')
|
|
resin = profile == 'elegoo_resin'
|
|
fields = {**ELEGOO_COMMON, **(ELEGOO_RESIN if resin else ELEGOO_FDM)} if elegoo else dict(NUMERIC)
|
|
if printer.get('dual_nozzle'):
|
|
fields.update(DUAL)
|
|
fields.update({key: EXTRA_FANS[key] for key in printer.get('extra_fans', [])})
|
|
expressions = {}
|
|
for key, suffix in fields.items():
|
|
sid = f'{pid}_{key}'
|
|
domain, suffix = suffix if isinstance(suffix, tuple) else ('sensor', suffix)
|
|
eid = entity(printer, key, suffix, domain)
|
|
package['sensor'].append({'platform': 'homeassistant', 'id': sid,
|
|
'entity_id': eid, 'internal': True})
|
|
expressions[key] = f'id({sid}).state'
|
|
if elegoo:
|
|
text_fields = {key: (entity(printer, key, suffix), None) for key, suffix in {
|
|
'status': 'print_status', 'task': 'file_name', 'stage': 'current_status',
|
|
'print_error': 'print_error', 'print_error_reason': 'error_status_reason'}.items()}
|
|
text_fields['online'] = (entity(printer, 'online', 'sdcp_status', 'binary_sensor'), None)
|
|
else:
|
|
text_fields = {key: (entity(printer, key, suffix), None) for key, suffix in TEXT.items()}
|
|
text_fields.update({key: (entity(printer, key, suffix, 'binary_sensor'), None)
|
|
for key, suffix in BINARY.items()})
|
|
hms_entity = entity(printer, 'hms_error', BINARY['hms_error'], 'binary_sensor')
|
|
text_fields['hms_count'] = (hms_entity, 'Count')
|
|
for number in range(1, 4):
|
|
for key, attribute in [('message', 'Error'), ('code', 'Code'), ('severity', 'Severity')]:
|
|
text_fields[f'hms_{number}_{key}'] = (hms_entity, f'{number}-{attribute}')
|
|
print_error_entity = entity(printer, 'print_error', BINARY['print_error'], 'binary_sensor')
|
|
text_fields['print_error_message'] = (print_error_entity, 'error')
|
|
text_fields['print_error_code'] = (print_error_entity, 'code')
|
|
for key, numeric_key in [('remaining_unit', 'time_left'), ('nozzle_unit', 'nozzle'),
|
|
('bed_unit', 'bed'), ('uv_unit', 'uv'), ('elapsed_unit', 'elapsed'),
|
|
('total_time_unit', 'total_time')]:
|
|
if numeric_key in fields:
|
|
suffix = fields[numeric_key]
|
|
domain, suffix = suffix if isinstance(suffix, tuple) else ('sensor', suffix)
|
|
text_fields[key] = (entity(printer, numeric_key, suffix, domain), 'unit_of_measurement')
|
|
trays = trays_for(printer)
|
|
for i, tray in enumerate(trays):
|
|
for key, attr in [('type', 'type'), ('color', 'color'), ('active', 'active')]:
|
|
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 = snapshot_lines(printer, expressions, trays)
|
|
snapshots.append((pid, list(lines)))
|
|
tray_widgets = ['{' + ', '.join(f'id({pid}_tray_{i}_{part})'
|
|
for part in ('card', 'swatch', 'label')) + '}'
|
|
for i in range(len(trays))]
|
|
refs = [f'id({pid}_{key})' for key in (
|
|
'status_label', 'task_label', 'top_progress', 'remaining_label', 'layers_label', 'stage_label',
|
|
'health_details')]
|
|
for key in ('progress', 'nozzle', 'bed', 'fan', 'aux', 'exhaust'):
|
|
refs.append('{' + ', '.join(f'id({pid}_{key}_{part})' for part in ('arc', 'label', 'detail')) + '}'
|
|
if not resin or key == 'progress' else '{}')
|
|
refs.append('{' + ', '.join(tray_widgets) + '}')
|
|
if resin:
|
|
for key in ('uv', 'layer'):
|
|
refs.append('{' + ', '.join(f'id({pid}_{key}_{part})' for part in ('arc', 'label', 'detail')) + '}')
|
|
refs.append(f'id({pid}_elapsed_label)')
|
|
lines.append('bambu_display::render(data, {' + ', '.join(refs) + '});')
|
|
package['script'].append({'id': f'{pid}_refresh', 'then': [{'lambda': Lambda('\n'.join(lines))}]})
|
|
package['interval'][0]['then'].append({'if': {
|
|
'condition': {'lvgl.page.is_showing': f'{pid}_page'},
|
|
'then': [{'script.execute': f'{pid}_refresh'}]}})
|
|
package['lvgl']['pages'].append(page_for(printer, index + 2, page_total))
|
|
package['script'].append({'id': 'fleet_refresh', 'then': [
|
|
{'lambda': Lambda('\n'.join(fleet_refresh_lines(snapshots)))}]})
|
|
package['interval'][0]['then'].append({'if': {
|
|
'condition': {'lvgl.page.is_showing': 'fleet_page'},
|
|
'then': [{'script.execute': 'fleet_refresh'}]}})
|
|
package['lvgl']['pages'].insert(0, overview_page(printers))
|
|
package['lvgl']['pages'].append(color_test_page())
|
|
package['button'] = [{'platform': 'template', 'name': 'Display Color Test',
|
|
'entity_category': 'diagnostic',
|
|
'on_press': [{'lvgl.page.show': 'color_test_page'}]}]
|
|
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 plus fleet overview).')
|
|
else:
|
|
destination.write_text(content)
|
|
print(f'Generated {destination.name} with {len(printers)} printer pages plus fleet overview.')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|