Derive Centauri progress from layers or print time when percent stays at zero.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Joshua King
2026-10-21 14:02:50 -04:00
co-authored by Cursor
parent 994cfb6b60
commit af7f268888
6 changed files with 113 additions and 15 deletions
+3 -1
View File
@@ -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
+43 -10
View File
@@ -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<HealthNotice> 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<Tray> 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<float>(elapsed_s) / static_cast<float>(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);
+5 -2
View File
@@ -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)
+39
View File
@@ -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);
+4 -2
View File
@@ -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:
+19
View File
@@ -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");