feat(shelly): optimize dew point ventilation setpoints, duty cycle, and night-window gating
- Raise target humidity setpoints from 40% to 55% with a 2% hysteresis shut-off - Eliminate 9999-min unlimited runtime bypass and strictly cap bursts at 35 min - Rebalance duty cycle to 1:1 ratio (35 min run, 35 min cooldown rest) - Implement Shelly internal RTC time-of-day check to restrict routine drying to 02:00-07:00 - Add delta-T guard requiring spread >= 3.5°C and max 20 min bursts when outside is >8°C colder - Update AGENTS.md documentation to reflect optimized gates and thresholds
This commit is contained in:
@@ -13,6 +13,10 @@ var MAX_HEAT_CEIL = ${max_indoor_temp_heating};
|
||||
var WINTER_IN_TH = ${winter_temp_threshold_in};
|
||||
var TARGET_SUMMER = ${target_humi_summer};
|
||||
var TARGET_WINTER = ${target_humi_winter};
|
||||
var URGENT_HUMI_TH = ${urgent_humi_threshold};
|
||||
var NIGHT_START_H = ${night_window_start_hour};
|
||||
var NIGHT_END_H = ${night_window_end_hour};
|
||||
var MAX_RUN_MIN = ${max_runtime_minutes};
|
||||
var MIN_PAUSE_MIN = ${min_pause_minutes};
|
||||
|
||||
var LIM_BASE = ${critical_humi_base};
|
||||
@@ -28,6 +32,16 @@ var st_run = 0, st_target = TARGET_SUMMER, st_mold_th = 70.0, st_mold_crit = 0,
|
||||
var st_quality = 0, st_max_run_min = 0, st_gov_state = 0;
|
||||
var pid_out = -1, pid_in = -1;
|
||||
|
||||
function getCurrentHour() {
|
||||
var s = Shelly.getComponentStatus("sys");
|
||||
if (!s || typeof s.time !== "string" || s.time.length < 2) return undefined;
|
||||
var h0 = s.time.charCodeAt(0) - 48;
|
||||
var h1 = s.time.charCodeAt(1) - 48;
|
||||
if (h0 < 0 || h0 > 9 || h1 < 0 || h1 > 9) return undefined;
|
||||
var h = h0 * 10 + h1;
|
||||
return (h >= 0 && h <= 23) ? h : undefined;
|
||||
}
|
||||
|
||||
function calcDewPoint(T, RH) {
|
||||
if (typeof T !== "number" || typeof RH !== "number" || isNaN(T) || isNaN(RH) || RH <= 0) return undefined;
|
||||
var a = (T >= 0) ? 17.27 : 21.875, b = (T >= 0) ? 237.7 : 265.5;
|
||||
@@ -60,6 +74,7 @@ function evaluateDecision() {
|
||||
var reqSpread = DP_SPREAD_BASE;
|
||||
var targetHumi = TARGET_SUMMER;
|
||||
var govState = 2; // Comfort default
|
||||
var deltaT = ti - to; // Positive when outside is colder than cellar
|
||||
|
||||
if (ti > MAX_HEAT_CEIL) {
|
||||
// Warm Zone (> 20°C): Maximize cooling. Prevent heating if To > Ti.
|
||||
@@ -71,8 +86,10 @@ function evaluateDecision() {
|
||||
// Comfort Zone (14°C - 20°C)
|
||||
govState = 2;
|
||||
targetHumi = TARGET_SUMMER;
|
||||
if (to < (ti - 5.0)) {
|
||||
reqSpread = DP_SPREAD_BASE + 1.0; // Strong cooling requires prime spread >= 3.0°C
|
||||
if (deltaT > 8.0) {
|
||||
reqSpread = DP_SPREAD_BASE + 1.5; // Very cold outside air requires premium spread >= 3.5°C
|
||||
} else if (deltaT > 5.0) {
|
||||
reqSpread = DP_SPREAD_BASE + 1.0; // Moderate cooling requires spread >= 3.0°C
|
||||
}
|
||||
} else {
|
||||
// Cold Zone (< 14°C)
|
||||
@@ -85,30 +102,44 @@ function evaluateDecision() {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Target Humidity Gate
|
||||
if (hi <= targetHumi) {
|
||||
// 3. Target Humidity Gate with Hysteresis (2% lower to stop when already ventilating)
|
||||
var effTarget = relay_on ? (targetHumi - 2.0) : targetHumi;
|
||||
if (hi <= effTarget) {
|
||||
return { run: false, reason: "TARGET_REACHED", maxRun: 0, qual: 0, gov: govState };
|
||||
}
|
||||
|
||||
// 4. Dew Point Spread Gate
|
||||
// 4. 2-Tier Scheduling: Urgent (>60% or mold crit) runs 24/7; Routine (<=60%) restricted to night window
|
||||
var isUrgent = (hi > URGENT_HUMI_TH || isMoldCrit);
|
||||
if (!isUrgent) {
|
||||
var curHour = getCurrentHour();
|
||||
if (typeof curHour === "number") {
|
||||
var inWindow = (NIGHT_START_H <= NIGHT_END_H)
|
||||
? (curHour >= NIGHT_START_H && curHour < NIGHT_END_H)
|
||||
: (curHour >= NIGHT_START_H || curHour < NIGHT_END_H);
|
||||
if (!inWindow) {
|
||||
return { run: false, reason: "OUTSIDE_NIGHT_WINDOW", maxRun: 0, qual: 0, gov: govState };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Dew Point Spread Gate
|
||||
var spread = di - do_;
|
||||
if (spread < reqSpread) {
|
||||
return { run: false, reason: "INSUFFICIENT_SPREAD", maxRun: 0, qual: Math.round(Math.max(0, spread * 20)), gov: govState };
|
||||
}
|
||||
|
||||
// 5. Dynamic Quality Runtime Formula
|
||||
// Runtime [min] = (spread - DP_SPREAD_BASE) * 45 min
|
||||
var maxRunMin = 0;
|
||||
if (spread >= 5.0) {
|
||||
maxRunMin = 9999; // Unlimited continuous run
|
||||
} else {
|
||||
maxRunMin = Math.round((spread - DP_SPREAD_BASE) * 45);
|
||||
if (maxRunMin < 15) maxRunMin = 15;
|
||||
}
|
||||
// 6. Dynamic Quality Runtime Formula with Strict Upper Cap (MAX_RUN_MIN)
|
||||
var maxRunMin = Math.round((spread - DP_SPREAD_BASE) * 20);
|
||||
if (maxRunMin < 15) maxRunMin = 15;
|
||||
if (maxRunMin > MAX_RUN_MIN) maxRunMin = MAX_RUN_MIN;
|
||||
|
||||
// Cold Zone burst throttle (max 25 min bursts when Ti < 14°C)
|
||||
if (ti < WINTER_IN_TH && maxRunMin > 25) {
|
||||
maxRunMin = 25;
|
||||
// Cold Zone burst throttle (max 20 min bursts when Ti < 14°C)
|
||||
if (ti < WINTER_IN_TH && maxRunMin > 20) {
|
||||
maxRunMin = 20;
|
||||
}
|
||||
// Large Delta T throttle (max 20 min bursts when outside is >8°C colder)
|
||||
if (deltaT > 8.0 && maxRunMin > 20) {
|
||||
maxRunMin = 20;
|
||||
}
|
||||
|
||||
var qualityScore = Math.min(100, Math.round(spread * 20));
|
||||
@@ -139,7 +170,7 @@ function applyDecision(d) {
|
||||
|
||||
if (d.run) {
|
||||
fan_run_sec += SCHALTZEIT_SEC;
|
||||
if (d.maxRun < 9999 && fan_run_sec >= d.maxRun * 60) {
|
||||
if (d.maxRun > 0 && fan_run_sec >= d.maxRun * 60) {
|
||||
d.run = false;
|
||||
last_reason = "BURST_LIMIT_REACHED";
|
||||
fan_pause_sec = MIN_PAUSE_MIN * 60;
|
||||
|
||||
Reference in New Issue
Block a user