Skip to content

Commit f849454

Browse files
committed
fix: Optimize WRG efficiency update rate, correct NTC mapping, and eliminate UI lag
1 parent b682ea5 commit f849454

12 files changed

Lines changed: 167 additions & 79 deletions

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.8.259] - 2026-05-13
8+
## [0.9.1] - 2026-05-14
9+
10+
### Fixed
11+
- **UI Latenz & Synchronisation**: Behebung drastischer Verzögerungen bei der Aktualisierung von Template-Sensoren im Home Assistant Dashboard (z.B. "Lüfter Richtung" und "WRG Effizienz").
12+
- **Event-Driven Template Sensors**: Fehlende `update_interval` Angaben führten dazu, dass Template-Sensoren, die auf C++ Variablen (`globals`) basieren, auf den langsamen 60-Sekunden Standard-Polling-Zyklus von ESPHome zurückfielen. Entsprechende Sensoren erzwingen nun via `update_interval: 1s` oder `5s` sofortige HA-Statusupdates.
13+
- **WRG-Effizienz Schwebung**: Ein statisches 60-Sekunden-Intervall führte dazu, dass die Effizienz-Berechnung fast nie das korrekte 42-Sekunden-Zuluft-Fenster traf. Dies wurde in einen Event-gesteuerten Modus mit 5-Sekunden-Takt (`update_interval: 5s`) umgeschrieben. Die Effizienzkurve wird nun live und hochauflösend gerendert.
914

15+
### Changed
16+
- **NTC Sensor Mapping**: Konsequenter Tausch der NTC-Rollen zur physischen Realität: `temp_abluft` repräsentiert die Referenz-Innentemperatur (gemessen während der Abluft-Phase), während `temp_zuluft` die Außentemperatur repräsentiert.
17+
- **Median-Filter Tuning**: Anpassung der Window-Send-Rate in `sensor_NTC.yaml` (`send_every: 3` -> `1`), um die trägen thermischen Messwerte mit der sehr kurzen 70-Sekunden Richtungsphase zu synchronisieren und das "Verschlucken" von gültigen Fenstern zu beenden.
18+
- **CO2 Bewertung**: Die Text-Klassifizierung ("Gut", "Schlecht") reagiert nun ohne 60-Sekunden Verzögerung latenzfrei auf Änderungen des darunterliegenden CO2-Wertes.
19+
20+
## [0.8.259] - 2026-05-13
1021
### Fixed
1122
- **UI Responsiveness**: Resolved an issue where the "Lüfter Intensität" slider and "ESP-NOW Peerprüfung" switch in Home Assistant would take up to 60 seconds to update or revert to their previous state.
1223
- Added explicit `publish_state()` calls to the `set_action` and `turn_on/off_action` blocks for template entities configured with `optimistic: false`.

components/helpers/auto_mode.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ inline void get_effective_temperatures(uint32_t now, float &eff_in, float &eff_o
138138
local_out = read_sensor(temp_zuluft);
139139
if (std::isnan(local_in)) local_in = read_sensor(temp_abluft);
140140
} else {
141-
local_out = read_sensor(temp_abluft);
142-
if (std::isnan(local_in)) local_in = read_sensor(temp_zuluft);
141+
local_out = read_sensor(temp_zuluft);
142+
if (std::isnan(local_in)) local_in = read_sensor(temp_abluft);
143143
}
144144
} else if (internal_mode == esphome::MODE_ECO_RECOVERY) {
145-
local_out = read_sensor(temp_abluft);
146-
if (std::isnan(local_in)) local_in = read_sensor(temp_zuluft);
145+
local_out = read_sensor(temp_zuluft);
146+
if (std::isnan(local_in)) local_in = read_sensor(temp_abluft);
147147
}
148148

149149
// Update controller state for networking

components/helpers/climate.h

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ inline std::string get_co2_classification(float co2_ppm) {
3535
return VentilationLogic::get_co2_classification(co2_ppm);
3636
}
3737

38+
// Stabilization parameter constants
39+
constexpr float NTC_MAX_DEVIATION = 0.3f;
40+
constexpr uint32_t NTC_MIN_WAIT_MS = 15000;
41+
constexpr size_t NTC_WINDOW_SIZE = 3;
42+
3843
/**
3944
* @brief Calculates the heat recovery efficiency fraction (0.0 – 1.0).
4045
*
@@ -48,7 +53,7 @@ inline std::string get_co2_classification(float co2_ppm) {
4853
* @param[in] t_aussen Outside ambient air temperature.
4954
* @param[in] current_eff The last calculated efficiency (to hold during flips).
5055
*/
51-
inline float calculate_heat_recovery_efficiency(float t_raum, float t_zuluft,
56+
inline float calculate_heat_recovery_efficiency(float t_raum, float t_zuluft_live,
5257
float t_aussen,
5358
float current_eff) {
5459
if (ventilation_ctrl == nullptr)
@@ -59,79 +64,67 @@ inline float calculate_heat_recovery_efficiency(float t_raum, float t_zuluft,
5964
const auto hw = ventilation_ctrl->state_machine.get_target_state(now);
6065

6166
const bool is_wrg = (mode == esphome::MODE_ECO_RECOVERY);
62-
const bool is_intake = hw.direction_in;
67+
const bool is_intake = hw.direction_in; // Zuluft-Phase (von außen nach innen)
6368

64-
// Abort if NTC sensors are disconnected (usually report ~87°C) or invalid
65-
if (std::isnan(t_zuluft) || std::isnan(t_aussen) || t_zuluft > 80.0f || t_aussen > 80.0f) {
66-
return NAN;
69+
// Abort if sensors are disconnected or invalid
70+
if (std::isnan(t_zuluft_live) || std::isnan(t_aussen) || std::isnan(t_raum)) {
71+
return current_eff;
6772
}
6873

69-
// Boot-Guard: Keine Messung in den ersten 60s nach Boot
74+
// Boot-Guard: Keine Messung in den ersten 60s
7075
static bool boot_complete = false;
7176
if (!boot_complete) {
7277
if (now < 60000) return current_eff;
7378
boot_complete = true;
7479
}
7580

76-
// Thermal stabilization: Wait at least 30s into the cycle before sampling
77-
constexpr uint32_t stable_time_ms = 30000;
7881
const uint32_t time_since_flip = now - last_direction_change_time;
79-
// Sicherstellen dass mindestens EIN echter Richtungswechsel stattgefunden hat
8082
const bool had_real_flip = (last_direction_change_time > 0);
81-
const bool is_stable = had_real_flip && (time_since_flip > stable_time_ms);
8283

84+
// Dynamische Wartezeit, um die NTC-Anpassungsphase nach dem Richtungswechsel auszulassen (analog filter_ntc_stable)
85+
const uint32_t cycle_ms = ventilation_ctrl->state_machine.cycle_duration_ms;
86+
uint32_t wait_ms = NTC_MIN_WAIT_MS;
87+
if (cycle_ms > 0) {
88+
wait_ms = std::clamp(
89+
static_cast<uint32_t>(cycle_ms * 0.4f),
90+
NTC_MIN_WAIT_MS,
91+
cycle_ms > 5000 ? cycle_ms - 5000 : cycle_ms / 2
92+
);
93+
}
94+
95+
const bool is_stable = had_real_flip && (time_since_flip > wait_ms);
96+
97+
// Messung nur während der Zuluft-Phase, NACH der Anpassungsphase
8398
if (is_wrg && is_intake && is_stable) {
84-
if (std::isnan(t_raum)) {
85-
ESP_LOGD("climate", "WRG Efficiency: Room sensor data NaN, holding: %.1f%%", current_eff);
86-
return current_eff;
87-
}
99+
// Effizienz berechnen. VentilationLogic gibt 0.0 bis 100.0 zurück.
88100
float eff = VentilationLogic::calculate_heat_recovery_efficiency(
89-
t_raum, t_zuluft, t_aussen);
101+
t_raum, t_zuluft_live, t_aussen);
90102

91-
// NaN abfangen (z.B. wenn T_raum == T_aussen -> Division durch 0)
92103
if (std::isnan(eff) || std::isinf(eff)) {
93-
ESP_LOGW("climate",
94-
"WRG efficiency calculation returned invalid value "
95-
"(T_raum=%.1f, T_zuluft=%.1f, T_aussen=%.1f) — holding %.1f%%",
96-
t_raum, t_zuluft, t_aussen, current_eff * 100.0f);
97104
return current_eff;
98105
}
99106

100107
// Physikalisch sinnvoller Bereich: 0% bis 110%
101-
if (eff < 0.0f || eff > 1.1f) {
108+
if (eff < 0.0f || eff > 110.0f) {
102109
ESP_LOGW("climate",
103110
"WRG efficiency out of plausible range: %.1f%% "
104-
"(T_raum=%.1f, T_zuluft=%.1f, T_aussen=%.1f)",
105-
eff * 100.0f, t_raum, t_zuluft, t_aussen);
106-
return current_eff; // Halte letzten gültigen Wert
111+
"(T_raum=%.1f, T_zuluft_live=%.1f, T_aussen=%.1f)",
112+
eff, t_raum, t_zuluft_live, t_aussen);
113+
return current_eff;
107114
}
108115

109-
// Soft-Clamp für Anzeige: Werte zwischen 0% und 100%
110-
eff = std::clamp(eff, 0.0f, 1.0f);
116+
// Soft-Clamp für Anzeige auf 0-100%
117+
eff = std::clamp(eff, 0.0f, 100.0f);
111118

112-
ESP_LOGD("climate", "WRG Efficiency update: %.1f%% (Room:%.1f Zuluft:%.1f Out:%.1f)",
113-
eff * 100.0f, t_raum, t_zuluft, t_aussen);
119+
ESP_LOGD("climate", "WRG Efficiency update: %.1f%% (Room:%.1f Zuluft_Live:%.1f Out:%.1f)",
120+
eff, t_raum, t_zuluft_live, t_aussen);
114121
return eff;
115122
}
116123

117-
// Debugging holding reasons (only in WRG mode to avoid log spam)
118-
if (is_wrg) {
119-
if (!is_intake) {
120-
ESP_LOGD("climate", "WRG Efficiency: Holding during exhaust phase (Abluft)");
121-
} else if (!is_stable) {
122-
ESP_LOGD("climate", "WRG Efficiency: Waiting for stabilization (%us remaining)",
123-
(stable_time_ms - time_since_flip) / 1000);
124-
}
125-
}
126-
127-
// Not in a valid sampling window → hold last known value
128124
return current_eff;
129125
}
130126

131-
// Stabilization parameter constants
132-
constexpr float NTC_MAX_DEVIATION = 0.3f;
133-
constexpr uint32_t NTC_MIN_WAIT_MS = 15000;
134-
constexpr size_t NTC_WINDOW_SIZE = 3;
127+
135128

136129
/**
137130
* @brief NTC Sliding Window Stabilization Filter.
@@ -142,7 +135,7 @@ constexpr size_t NTC_WINDOW_SIZE = 3;
142135
* to the ceramic core, which has high thermal inertia and
143136
* requires time to "settle" after each direction change.
144137
*
145-
* @param[in] sensor_idx 0 for temp_zuluft, 1 for temp_abluft.
138+
* @param[in] sensor_idx 0 for temp_abluft (NTC Innen), 1 for temp_zuluft (NTC Außen).
146139
* @param[in] new_value The raw value reported by the physical NTC component.
147140
*
148141
* @return The original value if stable, else an empty optional to discard the update.
@@ -167,6 +160,17 @@ inline esphome::optional<float> filter_ntc_stable(int sensor_idx,
167160
return new_value;
168161
}
169162

163+
// --- NEU: Richtungs-Sperre (Phase-Lock) ---
164+
// NTC 0 (Innen) misst die wahre Innentemperatur nur während der Abluft-Phase (Luft strömt nach außen)
165+
// NTC 1 (Außen) misst die wahre Außentemperatur nur während der Zuluft-Phase (Luft strömt nach innen)
166+
const auto hw = ventilation_ctrl->state_machine.get_target_state(millis());
167+
if (sensor_idx == 0 && hw.direction_in) {
168+
return {}; // Zuluft-Phase: Innensensor misst aufgewärmte Luft -> Wert für T_raum halten!
169+
}
170+
if (sensor_idx == 1 && !hw.direction_in) {
171+
return {}; // Abluft-Phase: Außensensor misst abgekühlte Luft -> Wert für T_aussen halten!
172+
}
173+
170174
const uint32_t cycle_ms = ventilation_ctrl->state_machine.cycle_duration_ms;
171175
if (cycle_ms == 0) return new_value;
172176

components/helpers/globals.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,8 @@ extern esphome::binary_sensor::BinarySensor *const radar_presence; ///< Presence
284284
extern esphome::template_::TemplateSensor
285285
*const effective_co2; ///< Unified CO2 sensor (SCD41 or BME680 fallback).
286286
extern esphome::sensor::Sensor *const temperature; ///< Room Temperature (SCD41)
287-
extern esphome::ntc::NTC *const temp_zuluft; ///< Supply Air Temperature (NTC Inside)
288-
extern esphome::ntc::NTC
289-
*const temp_abluft; ///< Exhaust Air Temperature (NTC Outside)
287+
extern esphome::ntc::NTC *const temp_zuluft; ///< Outside Air Temperature (NTC Outside)
288+
extern esphome::ntc::NTC *const temp_abluft; ///< Inside Air Temperature (NTC Inside)
290289
extern esphome::homeassistant::HomeassistantSensor
291290
*const outdoor_humidity; ///< Outdoor Humidity (HA)
292291
extern esphome::homeassistant::HomeassistantBinarySensor

manifest_example.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "thomasengeroff.ventosync",
3-
"version": "0.8.260",
3+
"version": "0.9.1",
44
"builds": [
55
{
66
"chipFamily": "ESP32-C6",
77
"ota": {
8-
"md5": "13e39d0e76ec8c94d4f8e4010d3db65b",
8+
"md5": "5c0cb56a10c13c513bc31ff1b7026136",
99
"path": "firmware.bin",
1010
"summary": "OTA update check frequency set to 15 minutes"
1111
}

packages/base/esp32c6_common.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ substitutions:
3030
<<: !include ../../version.json
3131

3232
hide_climate_sensors: "false"
33+
hide_ntc_sensors: "false"
3334
# Firmware variant identifier for OTA manifest selection.
3435
# Override in each variant YAML (e.g., ventosync-nosensor, ventosync-radar-only).
3536
firmware_variant: "ventosync-full"

packages/sensors/sensor_NTC.yaml

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,33 @@ sensor:
5151

5252
- platform: ntc
5353
sensor: ntc_innen_resistance
54-
name: "WRG Temperatur Zuluft (Innen)"
55-
id: temp_zuluft
54+
name: "Temp. Abluft (Innen)"
55+
id: temp_abluft
56+
internal: ${hide_ntc_sensors}
5657
calibration:
5758
b_constant: 3976 # Beta coefficient from datasheet
5859
reference_temperature: 25°C
5960
reference_resistance: 10kOhm
6061
accuracy_decimals: 1
6162
filters:
6263
- median:
63-
window_size: 5
64-
send_every: 3
64+
window_size: 3
65+
send_every: 1
6566
- lambda: |-
6667
return filter_ntc_stable(0, x);
6768
69+
- platform: ntc
70+
sensor: ntc_innen_resistance
71+
name: "Temp. Abluft (Innen) RAW"
72+
id: temp_abluft_raw
73+
internal: ${hide_ntc_sensors}
74+
calibration:
75+
b_constant: 3976 # Beta coefficient from datasheet
76+
reference_temperature: 25°C
77+
reference_resistance: 10kOhm
78+
accuracy_decimals: 1
79+
entity_category: diagnostic
80+
6881
- platform: adc
6982
id: ntc_innen_source
7083
pin: GPIO1 # Pin D1
@@ -83,20 +96,33 @@ sensor:
8396

8497
- platform: ntc
8598
sensor: ntc_aussen_resistance
86-
name: "WRG Temperatur Abluft (Außen)"
87-
id: temp_abluft
99+
name: "Temp. Zuluft (Außen)"
100+
id: temp_zuluft
101+
internal: ${hide_ntc_sensors}
88102
calibration:
89103
b_constant: 3976 # Beta coefficient from datasheet
90104
reference_temperature: 25°C
91105
reference_resistance: 10kOhm
92106
accuracy_decimals: 1
93107
filters:
94108
- median:
95-
window_size: 5
96-
send_every: 3
109+
window_size: 3
110+
send_every: 1
97111
- lambda: |-
98112
return filter_ntc_stable(1, x);
99113
114+
- platform: ntc
115+
sensor: ntc_aussen_resistance
116+
name: "Temp. Zuluft (Außen) RAW"
117+
id: temp_zuluft_raw
118+
internal: ${hide_ntc_sensors}
119+
calibration:
120+
b_constant: 3976 # Beta coefficient from datasheet
121+
reference_temperature: 25°C
122+
reference_resistance: 10kOhm
123+
accuracy_decimals: 1
124+
entity_category: diagnostic
125+
100126
- platform: adc
101127
id: ntc_aussen_source
102128
pin: GPIO0 # Pin D0

packages/sensors/sensors_climate.yaml

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,27 @@ sensor:
3737
- platform: template
3838
name: "WRG Effizienz"
3939
id: heat_recovery_efficiency
40-
internal: ${hide_climate_sensors}
40+
internal: ${hide_ntc_sensors}
4141
unit_of_measurement: "%"
4242
state_class: "measurement"
4343
accuracy_decimals: 1
4444
icon: "mdi:percent"
45-
update_interval: 60s
45+
update_interval: 5s
4646
lambda: |-
47-
float t_in = id(scd41_temperature).state;
48-
if (std::isnan(t_in)) t_in = id(bme680_temperature).state;
47+
// Raumtemperatur (NTC Innen gefiltert, geloggt während Abluft)
48+
float t_in = id(temp_abluft).state;
49+
// Außentemperatur (NTC Außen gefiltert, geloggt während Zuluft)
50+
float t_out = id(temp_zuluft).state;
51+
// Aktuelle Zulufttemperatur nach Keramiktausch (RAW, ungefiltert)
52+
float t_live = id(temp_abluft_raw).state;
4953
50-
// Abort calculation if no valid room temperature is available
51-
if (std::isnan(t_in)) return NAN;
54+
// Abort calculation if no valid base temperatures are available
55+
if (std::isnan(t_in) || std::isnan(t_out) || std::isnan(t_live)) return NAN;
5256
5357
return calculate_heat_recovery_efficiency(
54-
t_in, // Room temperature (SCD41 or BME680 only!)
55-
id(temp_zuluft).state, // Supply air temperature
56-
id(temp_abluft).state, // Exhaust air temperature
58+
t_in, // T_raum
59+
t_live, // T_zuluft (live)
60+
t_out, // T_aussen
5761
id(heat_recovery_efficiency).state
5862
);
5963
@@ -97,19 +101,16 @@ text_sensor:
97101
id: effective_co2_bewertung
98102
internal: ${hide_climate_sensors}
99103
icon: "mdi:circle-slice-8"
100-
update_interval: 60s
101104
lambda: |-
102105
return get_co2_classification(id(effective_co2).state);
103106
104107
# Human-readable info about which sensor is used for WRG efficiency
105108
- platform: template
106109
name: "WRG Referenz-Messpunkt"
107110
id: wrg_reference_sensor
108-
internal: ${hide_climate_sensors}
111+
internal: ${hide_ntc_sensors}
109112
icon: "mdi:target-variant"
110113
update_interval: 60s
111114
lambda: |-
112-
if (!std::isnan(id(scd41_temperature).state)) return std::string("SCD41");
113-
if (!std::isnan(id(bme680_temperature).state)) return std::string("BME680");
114-
return std::string("Kein Raumsensor");
115+
return std::string("NTC Sensoren");
115116

packages/ui/ui_controls.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ text_sensor:
235235
name: "Lüfter Richtung"
236236
id: direction_display
237237
icon: "mdi:directions-fork"
238+
update_interval: 1s
238239
lambda: |-
239240
static std::string last_state = "";
240241
std::string current_state = "";

0 commit comments

Comments
 (0)