diff --git a/esphome/bambu-display/README.md b/esphome/bambu-display/README.md index eae2d9a..6ae5933 100644 --- a/esphome/bambu-display/README.md +++ b/esphome/bambu-display/README.md @@ -175,7 +175,9 @@ exist; their mappings are in `EXTRA_FANS`. A remaining-time override also update Filament entries use the Bambu integration's `type`, `color` and `active` attributes. For Elegoo printers, set `profile: elegoo_fdm` or `profile: elegoo_resin` (the default -profile is `bambu`). These profiles map progress to `percent_complete`, job name to +profile is `bambu`). These profiles map progress to `percent_complete`. The Centauri Carbon leaves +that sensor at 0 for most of a job, so an active Elegoo print falls back to the +layer count and then to elapsed time divided by `total_print_time`. Job name maps to `file_name`, remaining time to `remaining_print_time`, and total layers to `total_layers`. The FDM profile reads targets from the HA `number` entities `target_nozzle_temp` and `target_bed_temp`; it only monitors their state. Per-metric diff --git a/esphome/bambu-display/dashboard.h b/esphome/bambu-display/dashboard.h index 7a4f314..1cd7ea6 100644 --- a/esphome/bambu-display/dashboard.h +++ b/esphome/bambu-display/dashboard.h @@ -122,9 +122,9 @@ struct Snapshot { std::string hms_count, print_error_message, print_error_code; std::string print_error_reason, uv_unit, elapsed_unit; bool elegoo = false, resin = false; - float uv = NAN, elapsed = NAN; + float uv = NAN, elapsed = NAN, total_time = NAN; std::vector health_notices; - std::string remaining_unit, nozzle_unit, bed_unit; + std::string remaining_unit, nozzle_unit, bed_unit, total_time_unit; std::string finish_time; float progress = NAN, nozzle = NAN, nozzle_target = NAN, bed = NAN, bed_target = NAN; float time_left = NAN, layer = NAN, total_layers = NAN, fan = NAN; @@ -134,6 +134,36 @@ struct Snapshot { std::vector trays; bool live() const { return connected && is_on(online) && valid_text(status); } + // Centauri firmware parks on milestone names such as preheating_completed for + // most of a job, so any status that is not a known idle or finished state is active. + bool job_active() const { + auto idle = [](const std::string &value) { + return value.empty() || value == "unknown" || value == "unavailable" || value == "none" || + value == "idle" || value == "standby" || value == "ready" || value == "complete" || + value == "completed" || value == "finish" || value == "finished" || value == "cancelled" || + value == "canceled" || value == "stopped" || value == "stop" || value == "failed"; + }; + const auto state = lower(status); + const auto machine = lower(stage); + if (machine == "printing" || machine == "paused" || machine == "busy" || machine == "heating" || + machine == "preheating" || machine == "running") + return true; + return !idle(state); + } + // The Centauri reports percent_complete as 0 while CurrentLayer stays 0. + // Layers, then elapsed time against the total, still describe the job. + float completion() const { + if (!elegoo) return progress; + if (std::isfinite(progress) && progress > 0.0f) return std::clamp(progress, 0.0f, 100.0f); + if (!job_active()) return std::isfinite(progress) ? 0.0f : NAN; + if (std::isfinite(layer) && std::isfinite(total_layers) && layer > 0.0f && total_layers > 0.0f) + return std::clamp(100.0f * layer / total_layers, 0.0f, 100.0f); + const int elapsed_s = duration_seconds(elapsed, elapsed_unit); + const int total_s = duration_seconds(total_time, total_time_unit); + if (elapsed_s > 0 && total_s > 0) + return std::clamp(100.0f * static_cast(elapsed_s) / static_cast(total_s), 0.0f, 100.0f); + return std::isfinite(progress) ? 0.0f : NAN; + } bool has_print_error() const { if (!elegoo) return is_on(print_error); return elegoo_fault(print_error) || elegoo_fault(print_error_reason) || lower(status) == "error"; @@ -225,8 +255,9 @@ struct Snapshot { } std::string overview_meta() const { if (!live()) return "--"; - const std::string progress_text = std::isfinite(progress) - ? number(std::clamp(progress, 0.0f, 100.0f)) + "%" : "--"; + const float percent = completion(); + const std::string progress_text = std::isfinite(percent) + ? number(std::clamp(percent, 0.0f, 100.0f)) + "%" : "--"; const std::string time_line = remaining(time_left, remaining_unit); if (!finish_time.empty() && time_line != "--") return progress_text + " " + finish_time + " " + time_line; @@ -325,9 +356,10 @@ inline void render(const Snapshot &data, const Widgets &widgets) { 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.progress.arc, lv_color_hex(color), LV_PART_INDICATOR); - lv_bar_set_value(widgets.top_progress, live ? gauge_value(data.progress, 100) : 0, LV_ANIM_OFF); - render_gauge(widgets.progress, live && std::isfinite(data.progress) - ? std::clamp(data.progress, 0.0f, 100.0f) : NAN, live ? data.progress : NAN, 100, + const float percent = data.completion(); + lv_bar_set_value(widgets.top_progress, live ? gauge_value(percent, 100) : 0, LV_ANIM_OFF); + render_gauge(widgets.progress, live && std::isfinite(percent) + ? std::clamp(percent, 0.0f, 100.0f) : NAN, live ? percent : NAN, 100, live ? remaining(data.time_left, data.remaining_unit) : "--"); set_text(widgets.task, live ? (valid_text(data.task) ? data.task : "No active print") : data.connected ? "Waiting for printer data" : "Waiting for Home Assistant"); @@ -377,12 +409,13 @@ inline void render(const Snapshot &data, const Widgets &widgets) { inline void render_overview(const Snapshot &data, const OverviewWidgets &widgets) { const bool live = data.live(); const uint32_t filament = data.active_filament_color(); - const float shown = live && std::isfinite(data.progress) - ? std::clamp(data.progress, 0.0f, 100.0f) : NAN; + const float percent = data.completion(); + const float shown = live && std::isfinite(percent) + ? std::clamp(percent, 0.0f, 100.0f) : NAN; const bool black = filament_is_black(filament); lv_obj_set_style_arc_color(widgets.arc, lv_color_hex(black ? RING_BACKING : TRACK), LV_PART_MAIN); lv_obj_set_style_arc_color(widgets.arc, lv_color_hex(filament), LV_PART_INDICATOR); - lv_arc_set_value(widgets.arc, live ? gauge_value(data.progress, 100) : 0); + lv_arc_set_value(widgets.arc, live ? gauge_value(percent, 100) : 0); if (black) { lv_obj_set_style_arc_color(widgets.backing, lv_color_hex(RING_BACKING), LV_PART_MAIN); lv_obj_set_style_arc_color(widgets.backing, lv_color_hex(RING_BACKING), LV_PART_INDICATOR); diff --git a/esphome/bambu-display/generate.py b/esphome/bambu-display/generate.py index de3814e..79dc491 100644 --- a/esphome/bambu-display/generate.py +++ b/esphome/bambu-display/generate.py @@ -24,8 +24,10 @@ 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'} + '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'} @@ -409,7 +411,8 @@ def generate(printers): 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')]: + ('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) diff --git a/esphome/bambu-display/generated.yaml b/esphome/bambu-display/generated.yaml index ad11961..3ec9425 100644 --- a/esphome/bambu-display/generated.yaml +++ b/esphome/bambu-display/generated.yaml @@ -224,6 +224,10 @@ sensor: id: centauri_carbon_total_layers entity_id: sensor.centauri_carbon_total_layers internal: true +- platform: homeassistant + id: centauri_carbon_total_time + entity_id: sensor.centauri_carbon_total_print_time + internal: true - platform: homeassistant id: centauri_carbon_nozzle entity_id: sensor.centauri_carbon_nozzle_temperature @@ -232,6 +236,10 @@ sensor: id: centauri_carbon_bed entity_id: sensor.centauri_carbon_bed_temperature internal: true +- platform: homeassistant + id: centauri_carbon_elapsed + entity_id: sensor.centauri_carbon_current_print_time + internal: true - platform: homeassistant id: centauri_carbon_nozzle_target entity_id: number.centauri_carbon_target_nozzle_temp @@ -268,6 +276,10 @@ sensor: id: saturn_total_layers entity_id: sensor.saturn_total_layers internal: true +- platform: homeassistant + id: saturn_total_time + entity_id: sensor.saturn_total_print_time + internal: true - platform: homeassistant id: saturn_uv entity_id: sensor.saturn_uv_led_temp @@ -1186,6 +1198,16 @@ text_sensor: entity_id: sensor.centauri_carbon_bed_temperature internal: true attribute: unit_of_measurement +- platform: homeassistant + id: centauri_carbon_elapsed_unit + entity_id: sensor.centauri_carbon_current_print_time + internal: true + attribute: unit_of_measurement +- platform: homeassistant + id: centauri_carbon_total_time_unit + entity_id: sensor.centauri_carbon_total_print_time + internal: true + attribute: unit_of_measurement - platform: homeassistant id: saturn_status entity_id: sensor.saturn_print_status @@ -1225,6 +1247,11 @@ text_sensor: entity_id: sensor.saturn_current_print_time internal: true attribute: unit_of_measurement +- platform: homeassistant + id: saturn_total_time_unit + entity_id: sensor.saturn_total_print_time + internal: true + attribute: unit_of_measurement script: - id: prints_charming_refresh then: @@ -1438,8 +1465,10 @@ script: data.time_left = id(centauri_carbon_time_left).state; data.layer = id(centauri_carbon_layer).state; data.total_layers = id(centauri_carbon_total_layers).state; + data.total_time = id(centauri_carbon_total_time).state; data.nozzle = id(centauri_carbon_nozzle).state; data.bed = id(centauri_carbon_bed).state; + data.elapsed = id(centauri_carbon_elapsed).state; data.nozzle_target = id(centauri_carbon_nozzle_target).state; data.bed_target = id(centauri_carbon_bed_target).state; data.fan = id(centauri_carbon_fan).state; @@ -1454,6 +1483,8 @@ script: data.remaining_unit = id(centauri_carbon_remaining_unit).state; data.nozzle_unit = id(centauri_carbon_nozzle_unit).state; data.bed_unit = id(centauri_carbon_bed_unit).state; + data.elapsed_unit = id(centauri_carbon_elapsed_unit).state; + data.total_time_unit = id(centauri_carbon_total_time_unit).state; data.elegoo = true; const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); const auto clock = id(ha_time).now(); @@ -1469,6 +1500,7 @@ script: data.time_left = id(saturn_time_left).state; data.layer = id(saturn_layer).state; data.total_layers = id(saturn_total_layers).state; + data.total_time = id(saturn_total_time).state; data.uv = id(saturn_uv).state; data.elapsed = id(saturn_elapsed).state; data.status = id(saturn_status).state; @@ -1480,6 +1512,7 @@ script: data.remaining_unit = id(saturn_remaining_unit).state; data.uv_unit = id(saturn_uv_unit).state; data.elapsed_unit = id(saturn_elapsed_unit).state; + data.total_time_unit = id(saturn_total_time_unit).state; data.elegoo = true; data.resin = true; const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); @@ -1695,8 +1728,10 @@ script: data.time_left = id(centauri_carbon_time_left).state; data.layer = id(centauri_carbon_layer).state; data.total_layers = id(centauri_carbon_total_layers).state; + data.total_time = id(centauri_carbon_total_time).state; data.nozzle = id(centauri_carbon_nozzle).state; data.bed = id(centauri_carbon_bed).state; + data.elapsed = id(centauri_carbon_elapsed).state; data.nozzle_target = id(centauri_carbon_nozzle_target).state; data.bed_target = id(centauri_carbon_bed_target).state; data.fan = id(centauri_carbon_fan).state; @@ -1711,6 +1746,8 @@ script: data.remaining_unit = id(centauri_carbon_remaining_unit).state; data.nozzle_unit = id(centauri_carbon_nozzle_unit).state; data.bed_unit = id(centauri_carbon_bed_unit).state; + data.elapsed_unit = id(centauri_carbon_elapsed_unit).state; + data.total_time_unit = id(centauri_carbon_total_time_unit).state; data.elegoo = true; const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); const auto clock = id(ha_time).now(); @@ -1725,6 +1762,7 @@ script: data.time_left = id(saturn_time_left).state; data.layer = id(saturn_layer).state; data.total_layers = id(saturn_total_layers).state; + data.total_time = id(saturn_total_time).state; data.uv = id(saturn_uv).state; data.elapsed = id(saturn_elapsed).state; data.status = id(saturn_status).state; @@ -1736,6 +1774,7 @@ script: data.remaining_unit = id(saturn_remaining_unit).state; data.uv_unit = id(saturn_uv_unit).state; data.elapsed_unit = id(saturn_elapsed_unit).state; + data.total_time_unit = id(saturn_total_time_unit).state; data.elegoo = true; data.resin = true; const int seconds_left = bambu_display::duration_seconds(data.time_left, data.remaining_unit); diff --git a/esphome/bambu-display/preview.py b/esphome/bambu-display/preview.py index b2e932b..8c01a69 100644 --- a/esphome/bambu-display/preview.py +++ b/esphome/bambu-display/preview.py @@ -22,12 +22,14 @@ def main(): demo_values = {'progress': 67, 'nozzle': 218, 'nozzle_target': 220, 'bed': 60, 'bed_target': 60, 'time_left': 1.75, 'layer': 134, 'total_layers': 200, 'fan': 80, 'aux_fan': 45, 'exhaust_fan': 30, 'left_nozzle': 219, 'left_target': 220, - 'right_nozzle': 37, 'right_target': 0, 'uv': 42, 'elapsed': 30} + 'right_nozzle': 37, 'right_target': 0, 'uv': 42, 'elapsed': 30, + 'total_time': 200} demo_text = {'status': 'running', 'task': 'Workshop organizer - plate 2', 'stage': 'printing', 'online': 'on', 'print_error': 'off', 'hms_error': 'off', 'hms_count': '0', 'print_error_message': '', 'print_error_code': '', 'remaining_unit': 'h', 'nozzle_unit': '°C', 'bed_unit': '°C', - 'uv_unit': '°C', 'elapsed_unit': 'min', 'print_error_reason': 'ok'} + 'uv_unit': '°C', 'elapsed_unit': 'min', 'total_time_unit': 'min', + 'print_error_reason': 'ok'} colors = ['#60DE9AFF', '#F4C96BFF', '#74A9E8FF', '#F8F4ECFF', '#DF8BBFFF', '#FA936BFF', '#8E79CAFF', '#4CC9C1FF'] for printer in printers: diff --git a/esphome/bambu-display/test_dashboard.cpp b/esphome/bambu-display/test_dashboard.cpp index 6389051..741654e 100644 --- a/esphome/bambu-display/test_dashboard.cpp +++ b/esphome/bambu-display/test_dashboard.cpp @@ -99,6 +99,25 @@ int main() { assert(data.status_color() == AMBER); data.status = "complete"; assert(data.status_text() == "COMPLETE"); + data.status = "preheating_completed"; + data.stage = "printing"; + data.progress = 0; + data.layer = 0; + data.total_layers = 400; + data.elapsed = 30; + data.elapsed_unit = "min"; + data.total_time = 60; + data.total_time_unit = "min"; + assert(std::fabs(data.completion() - 50.0f) < 0.01f); + data.layer = 100; + assert(std::fabs(data.completion() - 25.0f) < 0.01f); + data.progress = 67; + assert(std::fabs(data.completion() - 67.0f) < 0.01f); + data.progress = 0; + data.layer = 0; + data.status = "idle"; + data.stage = "idle"; + assert(data.completion() == 0.0f); data.online = "off"; assert(!data.live()); assert(data.status_text() == "PRINTER OFFLINE");