Opening a window adds heat exchange that the envelope model mistook for
poor insulation or a constant cooling bias.
windows.py adds windows as config subentries ("Add window"): a contact
sensor (optionally inverted; unknown/unavailable stays unknown, never
"closed"), its zone, the direction its outside faces (default 345°,
editable) and an opening factor. Each poll computes the share of the
interval each window was open (one change located by its timestamp, a
round trip is unknown), the wind exposure with proper circular angles
(meteorological "from" direction vs the window's facing) and a recovery
weight after closing that decays towards 0, pulling the air back
towards its pre-ventilation temperature. A bounded event log records
opening/closing, duration, temperatures before/lowest/at close/30 min
after, weather and other open windows.
While a zone's window is open, unknown or recovering, the base and
extended models skip it; a separate ventilation learner fits the
closed-window model's residual with natural exchange, wind speed, a
directional term bounded by the wind term (a leeward window still
ventilates), recovery, and optional gust excess and cross-ventilation
terms. Forecasts assume an open window closes after a configurable time
(default 15 min), then recovers. New poll outcome "ventilation".
Weather gains optional wind direction (degrees, 360 -> 0) and wind gust
sources with the same priorities and fallback; knots and ft/s accepted.
On a synthetic week with ventilation events every four hours and a
daily outdoor swing, the configured zone keeps loss/heat/bias close to
the truth and recovers the combined natural+wind effect within 15 %,
recovery within 15 % and direction within 30 %. The same data without
the window configured produces a -0.013 °C/5 min cooling bias and an
11 % inflated boiler effect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
"""Configuration constants for Heating MPC."""
|
|
DOMAIN = "heating_mpc"
|
|
CONF_CLIMATE = "climate_entity"
|
|
CONF_HEATER = "heater_entity"
|
|
CONF_OUTSIDE = "outside_entity" # v0.1 legacy single temperature source
|
|
CONF_ROOMS = "room_entities"
|
|
CONF_WEIGHTS = "weights"
|
|
CONF_HORIZON = "horizon_minutes"
|
|
CONF_MIN_ON = "minimum_on_minutes"
|
|
CONF_MIN_OFF = "minimum_off_minutes"
|
|
CONF_SWITCH_COST = "switching_cost"
|
|
CONF_HEAT_COST = "heating_cost"
|
|
CONF_ROOM_MAX_AGE = "room_max_age_minutes"
|
|
CONF_WEATHER_MAX_AGE = "weather_max_age_minutes"
|
|
# Stale room sensors: False leaves them out (re-weighting the other rooms),
|
|
# True keeps using their last value for sensors that report only on change.
|
|
CONF_ROOM_STALE_KEEP = "room_stale_use_last_value"
|
|
CONF_BAND = "comfort_band"
|
|
# Forecast assumption: extra electrical heat returns to baseline with this half-life.
|
|
CONF_INTERNAL_HALF_LIFE = "internal_heat_half_life_minutes"
|
|
# Forecast assumption: an open window closes after this many more minutes.
|
|
CONF_WINDOW_OPEN_MINUTES = "window_open_minutes"
|
|
# Time constant of air recovering towards the walls' temperature after closing.
|
|
CONF_RECOVERY_MINUTES = "recovery_minutes"
|
|
# Optional ventilation terms (each must earn its keep in the accuracy comparison).
|
|
CONF_VENT_WIND = "ventilation_wind"
|
|
CONF_VENT_GUST = "ventilation_gust"
|
|
CONF_VENT_CROSS = "ventilation_cross"
|
|
|
|
# Each source list is ordered: first valid sensor wins; remaining are fallbacks.
|
|
WEATHER_SOURCES = {
|
|
"temperature": "outside_temperature_sources",
|
|
"wind": "outside_wind_sources",
|
|
# Meteorological: the direction the wind blows FROM, degrees clockwise from north.
|
|
"wind_direction": "outside_wind_direction_sources",
|
|
"wind_gust": "outside_wind_gust_sources",
|
|
"clouds": "outside_clouds_sources",
|
|
"humidity": "outside_humidity_sources",
|
|
"feels_like": "outside_feelslike_sources",
|
|
"pressure": "outside_pressure_sources",
|
|
"visibility": "outside_visibility_sources",
|
|
"rain": "outside_rain_sources",
|
|
"snow": "outside_snow_sources",
|
|
}
|
|
WEATHER_MAIN = ("temperature", "wind", "wind_direction", "wind_gust", "clouds")
|
|
WEATHER_EXTRA = ("humidity", "feels_like", "pressure", "visibility", "rain", "snow")
|
|
|
|
DEFAULTS = {
|
|
CONF_ROOM_MAX_AGE: 30,
|
|
CONF_WEATHER_MAX_AGE: 120,
|
|
CONF_ROOM_STALE_KEEP: False,
|
|
CONF_HORIZON: 120,
|
|
CONF_MIN_ON: 15,
|
|
CONF_MIN_OFF: 10,
|
|
CONF_SWITCH_COST: 0.15,
|
|
CONF_HEAT_COST: 0.025,
|
|
CONF_BAND: 0.15,
|
|
CONF_INTERNAL_HALF_LIFE: 30,
|
|
CONF_WINDOW_OPEN_MINUTES: 15,
|
|
CONF_RECOVERY_MINUTES: 20,
|
|
CONF_VENT_WIND: True,
|
|
CONF_VENT_GUST: False,
|
|
CONF_VENT_CROSS: False,
|
|
}
|
|
# A room sensor silent this long is treated as dead even when keeping last values.
|
|
ROOM_STALE_KEEP_MAX_MINUTES = 24 * 60
|
|
SAMPLE_MINUTES = 5
|
|
# A room's model is trusted for forecasts after this many accepted samples.
|
|
READY_SAMPLES = 200
|
|
READY_ON_SAMPLES = 30
|
|
READY_OFF_SAMPLES = 30
|
|
# Outcome of each poll for learning (Status sample_result, Last Sample sensor).
|
|
SAMPLE_RESULTS = (
|
|
"waiting_for_inputs", "first_sample", "sensors_changed", "learned", "ventilation", "partly_rejected",
|
|
"interval_out_of_range", "relay_changed", "outside_source_changed", "lag_uncertain",
|
|
)
|
|
BLOCK_MINUTES = 15
|