feat: Temperaturen mit Nachkommastelle anzeigen (S3 5.6.4 / P4 1.12.0) #91

Merged
thomas merged 1 commits from feat/temperatur-nachkommastelle into main 2026-09-12 02:43:31 +02:00
5 changed files with 181 additions and 28 deletions

No files matched your search

+17
View File
@@ -1,3 +1,20 @@
Version 5.6.4:
- Dampf-/Thermoblock-Temperatur wird jetzt mit einer Nachkommastelle angezeigt - im Webinterface, in
den Verlaufskurven und auf dem Touch-Display. Bisher wurde der Messwert direkt nach dem Einlesen auf
ganze Grad gerundet; die Nachkommastelle war damit schon weg, bevor irgendeine Anzeige sie sehen
konnte. Die einzige Ausnahme war die Offset-Kompensation, die im kalten Bereich den Rohwert
durchreicht - daher stand beim Wasserkessel dort zufällig eine Nachkommastelle, im Betrieb aber nie.
- Das gilt genauso für den Wasserkessel und den Zusatzsensor (Gehäuse/Tassenablage), damit alle
Temperaturen einheitlich aufgelöst sind.
- Die Regelung bleibt unverändert: PID, Sicherheitsabschaltungen und Auto-Tune arbeiten weiter mit dem
auf ganze Grad gerundeten Messwert. Die feinere Zahl entsteht auf einem getrennten Anzeigeweg, damit
sich das Heizverhalten durch diese Änderung nicht verschiebt.
- Die letzte Stelle ist leicht geglättet. Ungefiltert würde sie ständig springen - das Thermoelement
am Dampfkessel liefert ein 0,25-Grad-Raster, der NTC am Wasserkessel das Rauschen des ADC. Ein
größerer Temperatursprung wird weiterhin sofort übernommen, die Anzeige läuft also nicht nach.
- Das kleine OLED zeigt weiterhin ganze Grad (dort ist kein Platz), rundet den Wert jetzt aber, statt
die Nachkommastelle abzuschneiden.
Version 5.6.3:
- Bug-Fix: Beim Aufwecken über das Touch-Display blieb die Auswahl „Espresso oder Cold Extraction"
manchmal aus, während sie über den Schalter kam. Ursache waren zwei unterschiedliche Bedingungen für
+60 -17
View File
@@ -369,7 +369,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
* Firmware-Informationen
************************************************************************************/
String version = "5.6.3";
String version = "5.6.4";
String versionHersteller = "Thomas Müller";
String versionHerstellerMail =
"<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>";
@@ -603,6 +603,10 @@ double SetpointWasser, InputWasser, OutputWasser;
double OffsetDampf = 0.0, OffsetWasser = 0.0;
double OffsetCase = 0.0;
double RawInputDampf = NAN, RawInputWasser = NAN;
// Geglaettete Rohwerte - ausschliesslich fuer die Anzeige. Die Regelung arbeitet weiter mit
// den auf ganze Grad gerundeten Input*-Werten, damit sich am Regelverhalten nichts aendert;
// hier entsteht nur die feinere Zahl fuers Auge.
double SmoothRawDampf = NAN, SmoothRawWasser = NAN, SmoothRawCase = NAN;
double displayReferenceRawDampf = NAN, displayReferenceRawWasser = NAN;
bool offsetCompensationEnabled = false;
const double OFFSET_COMPENSATION_MAX_REFERENCE_RAW_C = 40.0;
@@ -621,6 +625,33 @@ void updateOffsetCompensationReferences() {
}
}
// Tiefpass fuer die Anzeigewerte. Ohne ihn zappelt die Nachkommastelle dauernd: Das
// Thermoelement am Dampfkessel liefert ein 0,25-Grad-Raster, der NTC am Wasserkessel das
// Rauschen des ADC. Ein grosser Sprung (Kaltstart, Sensorwechsel, Bezug) wird sofort
// uebernommen, damit die Anzeige nicht hinterherlaeuft.
const double DISPLAY_TEMP_SMOOTHING = 0.25; // Gewicht des neuen Messwerts je Messtakt
const double DISPLAY_TEMP_JUMP_RESET_C = 5.0; // ab dieser Abweichung ungeglaettet uebernehmen
void updateSmoothedRawTemperature(double &smoothed, double measured) {
if (!isfinite(measured)) {
smoothed = measured;
return;
}
if (!isfinite(smoothed) || fabs(measured - smoothed) >= DISPLAY_TEMP_JUMP_RESET_C) {
smoothed = measured;
return;
}
smoothed += DISPLAY_TEMP_SMOOTHING * (measured - smoothed);
}
// Anzeigewerte auf eine Nachkommastelle festzurren, damit Weboberflaeche, Display und
// Verlaufskurven denselben Wert sehen und die letzte Stelle nicht zwischen den Kanaelen
// auseinanderlaeuft.
double quantizeDisplayTemperature(double value) {
if (!isfinite(value)) return value;
return round(value * 10.0) / 10.0;
}
double getDisplayTemperature(double correctedTemp, double rawTemp, double offset, double referenceRaw) {
if (!offsetCompensationEnabled || offset >= 0.0 || !isfinite(correctedTemp) || !isfinite(rawTemp) || !isfinite(referenceRaw)) {
return correctedTemp;
@@ -634,11 +665,17 @@ double getDisplayTemperature(double correctedTemp, double rawTemp, double offset
}
double getDisplayedWaterTemperature() {
return getDisplayTemperature(InputWasser, RawInputWasser, OffsetWasser, displayReferenceRawWasser);
return quantizeDisplayTemperature(getDisplayTemperature(
SmoothRawWasser + OffsetWasser, SmoothRawWasser, OffsetWasser, displayReferenceRawWasser));
}
double getDisplayedSteamTemperature() {
return getDisplayTemperature(InputDampf, RawInputDampf, OffsetDampf, displayReferenceRawDampf);
return quantizeDisplayTemperature(getDisplayTemperature(
SmoothRawDampf + OffsetDampf, SmoothRawDampf, OffsetDampf, displayReferenceRawDampf));
}
double getDisplayedCaseTemperature() {
return quantizeDisplayTemperature(SmoothRawCase + OffsetCase);
}
// PID-Konstanten (Regelungsparameter)
@@ -3243,6 +3280,7 @@ void resetToDefaults() {
digitalWrite(SSR_LIGHT_PIN, LOW);
standbyModeActive = false;
InputCase = NAN;
SmoothRawCase = NAN;
caseSensorError = false;
// PID Limits neu setzen (falls Fenstergröße geändert wurde)
@@ -5546,12 +5584,17 @@ void loop() {
RawInputWasser = readNTCTemperature();
InputDampf = round(RawInputDampf + OffsetDampf);
InputWasser = round(RawInputWasser + OffsetWasser);
updateSmoothedRawTemperature(SmoothRawDampf, RawInputDampf);
updateSmoothedRawTemperature(SmoothRawWasser, RawInputWasser);
updateOffsetCompensationReferences();
if (caseSensorEnabled) {
InputCase = round(readCaseTemperature() + OffsetCase);
double rawCase = readCaseTemperature();
InputCase = round(rawCase + OffsetCase);
updateSmoothedRawTemperature(SmoothRawCase, rawCase);
caseSensorError = (isnan(InputCase) || InputCase < 0.0);
} else {
InputCase = NAN;
SmoothRawCase = NAN;
caseSensorError = false;
}
yield();
@@ -6456,7 +6499,7 @@ if (scaleModeActive) {
display.println(F("Heizt auf 93 C"));
display.println(F("vor..."));
display.print(F("Wasser: "));
display.print((int)displayedWaterTemp);
display.print((int)lround(displayedWaterTemp));
display.print(F("/"));
display.print((int)cleaningAssistantWaterSetpoint);
display.print(F(" "));
@@ -6480,7 +6523,7 @@ if (scaleModeActive) {
display.print((remainingMs + 999UL) / 1000UL);
display.println(F(" s"));
display.print(F("Wasser: "));
display.print((int)displayedWaterTemp);
display.print((int)lround(displayedWaterTemp));
display.print(F("/"));
display.print((int)cleaningAssistantWaterSetpoint);
display.print(F(" "));
@@ -6522,7 +6565,7 @@ if (scaleModeActive) {
else { display.print(F("Init")); }
display.println();
display.print("Temperatur:");
display.print((int)(autoTuneWasserActive ? displayedWaterTemp : displayedSteamTemp));
display.print((int)lround(autoTuneWasserActive ? displayedWaterTemp : displayedSteamTemp));
display.print(F(" "));
display.print((char)247);
display.print(F("C"));
@@ -6553,7 +6596,7 @@ if (scaleModeActive) {
display.println(F("wenn Temp. erreicht!"));
display.println(F(" "));
display.println(F("Temperatur: "));
display.print((int)displayedWaterTemp);
display.print((int)lround(displayedWaterTemp));
display.print(F(" / "));
display.print((int)SetpointWasser);
display.print(F(" "));
@@ -6832,13 +6875,13 @@ if (scaleModeActive) {
else if (wasserSensorError) { display.print(F("Sensorfehler!")); }
else if (coldExtractionModeActive) {
// Cold Extraction: kein Sollwert-Vergleich sinnvoll, der Wasserkreis heizt nicht
display.print((int)displayedWaterTemp);
display.print((int)lround(displayedWaterTemp));
display.print(F(" "));
display.print((char)247);
display.print(F("C (COLD)"));
}
else {
display.print((int)displayedWaterTemp);
display.print((int)lround(displayedWaterTemp));
display.print(F(" / "));
display.print((int)SetpointWasser);
display.print(F(" "));
@@ -6857,7 +6900,7 @@ if (scaleModeActive) {
bool steamDelayActiveSystem = (dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL));
bool shouldShowDelayMessage = steamDelayActiveSystem && !steamDelayOverridden;
if (!shouldShowDelayMessage) {
display.print((int)displayedSteamTemp);
display.print((int)lround(displayedSteamTemp));
display.print(F(" / "));
display.print((int)SetpointDampf);
display.print(F(" "));
@@ -8311,9 +8354,9 @@ void recordResetCheckpoint(uint16_t checkpoint) {
if (steamHeatDisabledByUser) flags |= RESET_DIAG_FLAG_STEAM_HEAT_DISABLED;
rtcResetDiag.flags = flags;
rtcResetDiag.waterTempDeciC = encodeResetDiagSignedTenths((float)InputWasser);
rtcResetDiag.steamTempDeciC = encodeResetDiagSignedTenths((float)InputDampf);
rtcResetDiag.caseTempDeciC = encodeResetDiagSignedTenths((float)InputCase);
rtcResetDiag.waterTempDeciC = encodeResetDiagSignedTenths((float)getDisplayedWaterTemperature());
rtcResetDiag.steamTempDeciC = encodeResetDiagSignedTenths((float)getDisplayedSteamTemperature());
rtcResetDiag.caseTempDeciC = encodeResetDiagSignedTenths((float)getDisplayedCaseTemperature());
rtcResetDiag.weightDeciG = encodeResetDiagSignedTenths(currentWeightReading);
rtcResetDiag.scaleType = scaleType;
rtcResetDiag.wifiStatus = (int8_t)WiFi.status();
@@ -11533,7 +11576,7 @@ void handleChartData(AsyncWebServerRequest *request) {
if (!caseSensorEnabled || caseSensorError || isnan(InputCase)) {
response->print(F("null"));
} else {
snprintf(buffer, sizeof(buffer), "%.1f", InputCase);
snprintf(buffer, sizeof(buffer), "%.1f", getDisplayedCaseTemperature());
response->print(buffer);
}
@@ -16544,7 +16587,7 @@ void buildDashboardJson(char *buffer, size_t bufferSize) {
if (!caseTempDashboardActive || caseSensorError || isnan(InputCase)) {
offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTemp\":null,");
} else {
dtostrf(InputCase, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTemp\":%s,", floatBuf);
dtostrf(getDisplayedCaseTemperature(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTemp\":%s,", floatBuf);
}
// Shot-Daten
@@ -16973,7 +17016,7 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) {
if (!caseSensorEnabled || caseSensorError || isnan(InputCase)) {
offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTemp\":null,");
} else {
dtostrf(InputCase, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTemp\":%s,", floatBuf);
dtostrf(getDisplayedCaseTemperature(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTemp\":%s,", floatBuf);
}
offset += snprintf(buffer + offset, bufferSize - offset, "\"maintenanceInterval\":%d,", maintenanceInterval);
+13
View File
@@ -1,3 +1,16 @@
Version 1.12.0:
- Die Dampf-/Thermoblock-Temperatur wird jetzt mit einer Nachkommastelle angezeigt, wie die
Wassertemperatur. Das gilt für die große Zahl auf der Startseite, den Sollwert darunter und
die Beschriftung der Verlaufskurve. Der Wert kam schon immer fein genug über die Verbindung
zur Maschine an - nur die Anzeige hat die Stelle weggeworfen. Voraussetzung ist die
Maschinen-Firmware 5.6.4, davor liefert die Maschine immer ganze Grad.
- Die große Temperaturzahl verkleinert sich automatisch, wenn sie sonst nicht mehr in die
Karte passt. "124,5°C" ist rund ein Drittel breiter als "124°C", und in der randlosen
Flächen-Darstellung reicht die halbe Bildschirmbreite dafür nicht. Bisher brach LVGL die
Zeile dann um und die zweite Zeile fiel aus der Karte - das betraf schon vorher die
Wassertemperatur, sobald sie 100 °C erreichte.
- Im Reinigungsassistenten steht die Wassertemperatur ebenfalls mit einer Nachkommastelle.
Version 1.11.3:
- Der Punkt sitzt wieder genau auf dem letzten Messwert, statt eine halbe Punktbreite davor.
Die Linie schaut damit rechts nicht mehr unter ihm hervor. Damit trotzdem nichts
+1 -1
View File
@@ -126,7 +126,7 @@
// -------------------------------------------------------------------------------------
// Firmware
// -------------------------------------------------------------------------------------
#define DISPLAY_FW_VERSION "1.11.3" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
#define DISPLAY_FW_VERSION "1.12.0" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
// -------------------------------------------------------------------------------------
// Protokoll
+90 -10
View File
@@ -1098,6 +1098,9 @@ static lv_obj_t* tempField(lv_obj_t* parent, const char* title, lv_color_t accen
// auseinandergezogen; ein paar Pixel weniger binden die Ziffern zu einer Zahl zusammen.
if (valueFont == &lv_font_maven_pro_bold_140) lv_obj_set_style_text_letter_space(*big, -3, 0);
else if (valueFont == &lv_font_maven_pro_bold_96) lv_obj_set_style_text_letter_space(*big, -2, 0);
// Ausgangsschrift merken: big_temp_apply() darf nur von hier aus verkleinern und muss
// zurueckfinden, sobald der Text wieder kuerzer wird.
lv_obj_set_user_data(*big, (void*)valueFont);
*setp = lv_label_create(c);
lv_label_set_text(*setp, "Soll --.-");
@@ -1120,6 +1123,79 @@ static lv_obj_t* tempField(lv_obj_t* parent, const char* title, lv_color_t accen
return c;
}
static int32_t big_temp_letter_space(const lv_font_t* f) {
if (f == &lv_font_maven_pro_bold_140) return -3;
if (f == &lv_font_maven_pro_bold_96) return -2;
return 0;
}
// Schriftleiter fuer die grosse Temperaturzahl, von gross nach klein.
static const lv_font_t* const kBigTempLadder[] = {
&lv_font_maven_pro_bold_140, &lv_font_maven_pro_bold_96,
&lv_font_maven_pro_48, &lv_font_maven_pro_40,
&lv_font_maven_pro_28
};
static const int kBigTempLadderLen = (int)(sizeof(kBigTempLadder) / sizeof(kBigTempLadder[0]));
static const lv_font_t* big_temp_base(lv_obj_t* label) {
const lv_font_t* base = (const lv_font_t*)lv_obj_get_user_data(label);
return base ? base : lv_obj_get_style_text_font(label, LV_PART_MAIN);
}
// Kleinste Stufe, in die der Text noch ohne Umbruch passt. Breite UND Hoehe der
// Beschriftung sind fest (siehe tempField) - laeuft der Text ueber, bricht LVGL die Zeile
// um und die zweite Zeile faellt aus der Karte heraus, statt nur eng zu wirken. Mit der
// Nachkommastelle ist "124,5°C" rund ein Drittel breiter als "124°C"; in der randlosen
// Flaechen-Darstellung reicht die halbe Bildschirmbreite dafuer nicht mehr.
// -1 heisst "nicht entscheidbar" - dann bleibt es bei der Ausgangsschrift.
static int big_temp_fit_index(lv_obj_t* label, const char* txt) {
const lv_font_t* base = big_temp_base(label);
int idx = 0;
while (idx < kBigTempLadderLen && kBigTempLadder[idx] != base) idx++;
if (idx >= kBigTempLadderLen) return -1; // Schrift nicht in der Leiter
int32_t avail = lv_obj_get_content_width(label);
if (avail <= 0) return idx; // vor dem ersten Layout noch unbekannt
for (; idx < kBigTempLadderLen - 1; idx++) {
lv_point_t sz;
lv_text_get_size(&sz, txt, kBigTempLadder[idx], big_temp_letter_space(kBigTempLadder[idx]),
0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
if (sz.x <= avail) break;
}
return idx;
}
static void big_temp_apply(lv_obj_t* label, int idx, const char* txt) {
const lv_font_t* base = big_temp_base(label);
const lv_font_t* pick = (idx >= 0 && idx < kBigTempLadderLen) ? kBigTempLadder[idx] : base;
if (lv_obj_get_style_text_font(label, LV_PART_MAIN) != pick) {
lv_obj_set_style_text_font(label, pick, 0);
lv_obj_set_style_text_letter_space(label, big_temp_letter_space(pick), 0);
// Die Hoehe bleibt die der Ausgangsschrift (sonst raeumt LVGL das Layout der ganzen
// Seite neu). Die kleinere Zahl wird darin per Innenabstand mittig gehalten, damit
// sie nicht an der Oberkante klebt.
int32_t padTop = (lv_font_get_line_height(base) - lv_font_get_line_height(pick)) / 2;
lv_obj_set_style_pad_top(label, padTop > 0 ? padTop : 0, 0);
}
label_set_if_changed(label, txt);
}
// Setzt beide grossen Temperaturzahlen gemeinsam. Gleich grosse Felder muessen gleich gross
// bleiben: Muss eine Zahl kleiner gesetzt werden, geht die andere mit. Sonst schrumpfte
// allein der Dampfkessel, weil "125,5°C" breiter ist als "93,5°C" - zwei verschieden
// grosse Zahlen nebeneinander sehen aus wie ein Fehler. Wo die Felder absichtlich ungleich
// sind (gewichtete Darstellung), bleibt jede Zahl bei ihrer eigenen Stufe.
static void big_temp_set_pair(lv_obj_t* a, const char* ta, lv_obj_t* b, const char* tb) {
if (!a || !b || !ta || !tb) return;
int ia = big_temp_fit_index(a, ta);
int ib = big_temp_fit_index(b, tb);
if (ia >= 0 && ib >= 0 && big_temp_base(a) == big_temp_base(b)) {
int common = (ia > ib) ? ia : ib;
ia = ib = common;
}
big_temp_apply(a, ia, ta);
big_temp_apply(b, ib, tb);
}
// Flache Verlaufskurve unter einer Temperatur. Zeigt, ob die Regelung haelt - das ist die
// eigentliche Frage an eine PID-Regelung, und eine Zahl kann sie grundsaetzlich nicht
// beantworten. Gespeist wird sie aus derselben Sekundenabtastung wie die Verlaufsseite.
@@ -2202,7 +2278,7 @@ static void chart_sample_cb(lv_timer_t*) {
(int)(dw + 0.5f), (int)(dd + 0.5f));
if (g_legW) lv_label_set_text(g_legW, ("Wasser " + tempStr(g_state.tempW, 1)).c_str());
if (g_legD) lv_label_set_text(g_legD, ("Dampf " + tempStr(g_state.tempD, 0)).c_str());
if (g_legD) lv_label_set_text(g_legD, ("Dampf " + tempStr(g_state.tempD, 1)).c_str());
if (g_legC) {
if (caseOn) {
const char* name = (g_state.caseSensorType == 1) ? "Tassenablage " : "Gehäuse ";
@@ -2766,13 +2842,13 @@ static void cln_refresh() {
String s;
if (st.cleaningWaiting) {
s = "Heizt auf 93 °C vor...\n\n";
s += "Wasser: " + tempStr(st.tempW, 0) + " / 93°C\n\n";
s += "Wasser: " + tempStr(st.tempW, 1) + " / 93°C\n\n";
s += "Die Zyklen starten automatisch,\nsobald die Temperatur erreicht ist.";
} else {
s = "Zyklus " + String(st.cleaningCycle) + " / " + String(st.cleaningCycles) + "\n\n";
s += "Phase: " + String(st.cleaningBrewPhase ? "Bezug" : "Pause") + "\n";
s += "Rest: " + String(st.cleaningPhaseRemainSec) + " s\n\n";
s += "Wasser: " + tempStr(st.tempW, 0) + " / 93°C\n";
s += "Wasser: " + tempStr(st.tempW, 1) + " / 93°C\n";
s += "Dampfheizung aus";
}
if (g_clnProgLbl) label_set_if_changed(g_clnProgLbl, s.c_str());
@@ -4749,14 +4825,20 @@ void ui_update(const MachineState& st) {
// Dashboard - Temperaturkarten (im Fehlerfall Warnsymbol + Klartext statt Zahl, wie OLED)
if (lblTempW) {
if (st.wasserSafetyShutdown || st.wasserSensorError) {
label_set_if_changed(lblTempW, LV_SYMBOL_WARNING);
// Beide grossen Zahlen zusammen setzen, damit sie bei knapper Kartenbreite
// gemeinsam eine Stufe kleiner werden (siehe big_temp_set_pair).
const bool warnW = st.wasserSafetyShutdown || st.wasserSensorError;
const bool warnD = st.dampfSafetyShutdown || st.dampfSensorError;
String bigW = warnW ? String(LV_SYMBOL_WARNING) : tempStr(st.tempW, 1);
String bigD = warnD ? String(LV_SYMBOL_WARNING) : tempStr(st.tempD, 1);
big_temp_set_pair(lblTempW, bigW.c_str(), lblTempD, bigD.c_str());
if (warnW) {
label_color_if_changed(lblTempW, COL_DANGER);
label_set_if_changed(lblSetW, st.wasserSafetyShutdown ? "Sicherheitsabsch." : "Sensorfehler!");
label_color_if_changed(lblSetW, COL_DANGER);
temp_progress_set(barDutyW, arcTempW, bulbTempW, 0, COL_DANGER);
} else {
label_set_if_changed(lblTempW, tempStr(st.tempW, 1).c_str());
if (st.cxActive) {
// Cold Extraction: der Sollwert ist bedeutungslos, es wird nicht geheizt.
// Statt eines Zielbands, das nie erreicht wird, den Grund anzeigen.
@@ -4775,17 +4857,15 @@ void ui_update(const MachineState& st) {
temp_progress_set(barDutyW, arcTempW, bulbTempW, pctW, colW);
}
}
if (st.dampfSafetyShutdown || st.dampfSensorError) {
label_set_if_changed(lblTempD, LV_SYMBOL_WARNING);
if (warnD) {
label_color_if_changed(lblTempD, COL_DANGER);
label_set_if_changed(lblSetD, st.dampfSafetyShutdown ? "Sicherheitsabsch." : "Sensorfehler!");
label_color_if_changed(lblSetD, COL_DANGER);
temp_progress_set(barDutyD, arcTempD, bulbTempD, 0, COL_DANGER);
} else {
label_set_if_changed(lblTempD, tempStr(st.tempD, 0).c_str());
lv_color_t colD = temp_state_color(st.tempD, st.setD);
label_color_if_changed(lblTempD, temp_value_color(colD));
label_set_if_changed(lblSetD, ("Soll " + tempStr(st.setD, 0)).c_str());
label_set_if_changed(lblSetD, ("Soll " + tempStr(st.setD, 1)).c_str());
label_color_if_changed(lblSetD, COL_TEXT_DIM);
int pctD = (st.setD > 0.0f) ? (int)(st.tempD / st.setD * 100.0f + 0.5f) : 0;
temp_progress_set(barDutyD, arcTempD, bulbTempD, pctD, colD);