Add a fleet overview page that shows every printer's status at once.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -121,7 +121,7 @@ def fdm_gauges(printer):
|
||||
return widgets
|
||||
|
||||
|
||||
def page_for(printer, index, total):
|
||||
def page_for(printer, display_index, total):
|
||||
pid = printer['id']
|
||||
resin = printer.get('profile') == 'elegoo_resin'
|
||||
widgets = [
|
||||
@@ -129,7 +129,7 @@ def page_for(printer, index, total):
|
||||
'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'{index + 1} / {total}', 520, 24, 70, 14, text_color=MUTED, text_align='CENTER'),
|
||||
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,
|
||||
@@ -213,6 +213,116 @@ def page_for(printer, index, total):
|
||||
'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_layout(count):
|
||||
row_h = (436 - 46) // count
|
||||
return row_h, row_h >= 52
|
||||
|
||||
|
||||
def overview_row(printer, y, row_h, two_line):
|
||||
pid = printer['id']
|
||||
name_w, status_x, status_w = (430, 448, 312) if two_line else (240, 258, 150)
|
||||
meta_x, meta_w, meta_y = (448, 312, 28) if two_line else (414, 346, 8)
|
||||
children = [
|
||||
{'obj': {'id': f'{pid}_overview_accent', 'x': 0, 'y': 8, 'width': 4,
|
||||
'height': max(row_h - 16, 8), 'bg_color': MUTED, 'bg_opa': 'COVER',
|
||||
'border_width': 0, 'radius': 2, 'pad_all': 0, 'scrollable': False,
|
||||
'gesture_bubble': True}},
|
||||
label(printer['name'], 12, 2 if two_line else 6, name_w, 18 if two_line else 16),
|
||||
label('SYNCING', status_x, 6 if two_line else 8, status_w, 16 if two_line else 14,
|
||||
id=f'{pid}_overview_status', text_align='RIGHT', text_color=MUTED),
|
||||
]
|
||||
if two_line:
|
||||
children.append(label('Waiting for Home Assistant', 12, 28, 430, 12,
|
||||
id=f'{pid}_overview_task', text_color=MUTED))
|
||||
children += [
|
||||
label('--', meta_x, meta_y, meta_w, 12 if two_line else 14,
|
||||
id=f'{pid}_overview_meta', text_align='RIGHT', text_color=MUTED),
|
||||
{'bar': {'id': f'{pid}_overview_progress', 'x': 12, 'y': row_h - 7, 'width': 748, 'height': 4,
|
||||
'min_value': 0, 'max_value': 100, 'value': 0, 'bg_color': TRACK,
|
||||
'indicator': {'bg_color': GREEN}, 'gesture_bubble': True}},
|
||||
]
|
||||
return {'obj': {'id': f'{pid}_overview_row', 'x': 8, 'y': y, 'width': 784, 'height': row_h,
|
||||
'bg_color': BG, 'bg_opa': 'COVER', 'border_width': 0, 'pad_all': 0,
|
||||
'radius': 0, 'scrollable': False, 'clickable': True, 'gesture_bubble': True,
|
||||
'on_click': [{'lvgl.page.show': f'{pid}_page'}], 'widgets': children}}
|
||||
|
||||
|
||||
def overview_page(printers):
|
||||
row_h, two_line = overview_layout(len(printers))
|
||||
widgets = [
|
||||
label('FLEET', 16, 8, 180, 26, text_color=GREEN),
|
||||
label('SYNCING', 210, 16, 574, 14, id='fleet_summary', text_align='RIGHT', text_color=MUTED),
|
||||
]
|
||||
widgets += [overview_row(printer, 46 + index * row_h, row_h, two_line)
|
||||
for index, printer in enumerate(printers)]
|
||||
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):
|
||||
_, two_line = overview_layout(len(snapshots))
|
||||
lines = ['bambu_display::FleetCounts counts;']
|
||||
for pid, snapshot in snapshots:
|
||||
task = f'id({pid}_overview_task)' if two_line else 'nullptr'
|
||||
lines.append('{')
|
||||
lines.extend(snapshot)
|
||||
lines.append('bambu_display::tally(counts, data);')
|
||||
lines.append('bambu_display::render_overview(data, {' + ', '.join([
|
||||
f'id({pid}_overview_status)', task, f'id({pid}_overview_meta)',
|
||||
f'id({pid}_overview_progress)', f'id({pid}_overview_accent)']) + '});')
|
||||
lines.append('}')
|
||||
lines.append('bambu_display::render_fleet_summary(id(fleet_summary), counts);')
|
||||
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.',
|
||||
@@ -236,6 +346,8 @@ 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')
|
||||
@@ -287,34 +399,11 @@ def generate(printers):
|
||||
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_', '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");',
|
||||
]
|
||||
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')) + '}')
|
||||
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')]
|
||||
@@ -331,7 +420,13 @@ def generate(printers):
|
||||
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)))
|
||||
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',
|
||||
@@ -351,10 +446,10 @@ def main():
|
||||
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).')
|
||||
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.')
|
||||
print(f'Generated {destination.name} with {len(printers)} printer pages plus fleet overview.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user