A climate control app for Home Assistant built with AppDaemon.
One app controls one climate entity: a heater (floor heating, panel heater) or an HVAC unit (heat pump, air conditioner). It sets the temperature from an indoor sensor or from a table of outdoor temperatures, switches HVAC between heat, cool and fan_only, closes and opens screening covers from lux, rain, wind and cloud cover, and follows vacation, open windows, time periods and the save / spend / reduce states from ElectricalManagement. Outdoor values come from the ad-Weather app.
π€ Set up with an AI assistant: the LLM guide helps an AI assistant find your own entity ids in Home Assistant and write the configuration. Notes for the assistant about this app: LLM.md.
- Breaking Changes
- Features
- Requirements
- Installation
- Quick start
- One app per device
- How the setpoint is found
- Adaptive controller (optional)
- HVAC modes, fan and preset
- Screens and covers
- Weather integration
- ElectricalManagement
- Configuration reference
- Full example configurations
- Validation
- Notifications
- Troubleshooting
- Notes on deliberate behaviour
- License
- Contributing
Your configuration keeps working without changes, but some defaults behave differently. The ones you notice most, and how to get the old behaviour back:
- Indoor mode runs every 30 minutes instead of 12:
interval_minutes: 12. daytime_increasingholds the room 0.3 warmer instead of 0.5 (not configurable).- The hold after a
daytime_increasingperiod lasts 2 hours instead of 1 (counted from when the increase stops applying, also whendaytime_savingsstart and replace it, only after a period that applied, and none for an HVAC infan_onlyat that moment):increase_hold_hours: 1. - An HVAC in heat lowers its setpoint before it switches to
fan_only:options: [has_fireplace]switches at once at target + 0.6, as before.
Everything that is new or different:
- One app per climate entity is the recommended setup: top-level
climate:andtype: heaterortype: hvac, see One app per device. The old style withHVAC:/Heaters:lists in one app still works but logs a deprecation WARNING. - Heaters with an outdoor-temperature table (
temperatures,target_heater_temp,save_temp, ...). This is the heater logic from ElectricalManagement. See How the setpoint is found.- Table mode: with no save or vacation temperature configured, the climate's own
min_tempis used (before: a fixed 10). The window and indoor corrections together lower the setpoint by at most 3 degrees.
- Table mode: with no save or vacation temperature configured, the climate's own
- Bug fixes in the same release (they apply to every setup):
- The JSON file is written through a temporary file and replaced, so a crash during the write can no longer destroy the learned data.
- HVAC in table mode now follows
reducefrom ElectricalManagement (the lowest of the save and vacation temperature), as table heaters do. Before,reducewas ignored. - Table mode: an offline
window_sensor_tempno longer lowers the setpoint (it counts as exactly at the threshold). Before, it was compared against the heater target instead of the indoor target. - A stale indoor sensor with an unavailable backup sensor now uses the stale value (as documented). Before, no value was used and the run was skipped.
- The setpoint set when an HVAC returns from
fan_onlyto heat is rounded to 0.5 like every other command (before, a value such as 21.3 was sent and sent again). - A missing or unavailable climate entity is logged once (WARNING for missing, INFO for unavailable), not silently.
- An HVAC in table mode without
vacation_tempno longer fails on vacation (falls back to 10). - With
controller: adaptivethe room trigger closes screens again after a vacation. Before, it stayed off from the end of the first vacation until a restart. - At startup the lux from the Weather app's last values is used at once. Before, it was 0 until the next
WEATHER_CHANGEevent: screens saw lux 0, and the adaptive controller used the clock (day / night) instead of lux.
- Save / spend / reduce from ElectricalManagement (optional, ElectricalManagement 1.0.5 or later), see ElectricalManagement.
- The app now has three more files,
cc_devices.py,cc_config.pyandcc_control.py, next toclimateCommander.pyandweather_data.py. Copy the whole app folder when you update (git pulldoes). - New, optional:
controller: shadow/controller: adaptive, a different way to find the setpoint, per device. The defaultclassicis the controller described in this README. See Adaptive controller. - The configuration is validated with pydantic (
cc_config.py). A value of the wrong type (for examplewindow_offset: abc, or a table entry withoutout) now logs an ERROR naming the key: an invalid device is skipped, an invalid app-level key stops that app. Before, such a value was silently used or crashed later (vacation_temp: abcused to fall back to 10). A single entity where a list is expected (for examplewindowsensors: binary_sensor.x) is accepted as a one-item list. See Validation. - Screens react to the room sensor between runs, see Room trigger. Nothing to configure. In short: screens close within seconds of the room getting warm (and bright) instead of up to one interval later, and on borderline days with a sensor in the airflow they may close more often than before.
- Table mode heaters now close screens (they never did): the same rule as an indoor mode heater, with
indoor_sensor_tempandtarget_indoor_temp. - Indoor mode heaters, open window: the setpoint is the lowest of
vacation_tempandsave_temp(whensave_tempis set). Before it was alwaysvacation_temp, so a heater with avacation_tempabove itssave_tempheated more for an open window than for a save period. Nothing changes withoutsave_temp. Table mode did this already; HVAC goes tofan_only. - Heaters stop waiting for lux when the room is not warm. Before, after one attempt that failed on lux, any later bright
WEATHER_CHANGEcould close a heater's screens with no check of the room, until a restart. Withcontroller: adaptivea heater on vacation uses the vacation rule (room above the target). - New key
cloud_cover(default90): screens close only while the cloud cover from the Weather app is below this percentage. Before, the limit was fixed at 90, so the default keeps the old behaviour. See Screens and covers. - Behaviour changes in the indoor-target mode (HVAC, and heaters without a table):
- The learned setpoints are no longer updated on vacation, with open windows, during
daytime_savings/daytime_increasingor while ElectricalManagement sendssave,spendorreduce. Before, vacation temperatures were learned and pulled the setpoint down after a vacation. - A room that stays 0.3 degrees or more away from the target is now corrected when the setpoint has not changed for 30 minutes. Before, nothing happened while the temperature did not move.
backup_indoor_sensor_tempis used only when the indoor sensor is unavailable or has not reported for an hour (last_reported). Before, a steady temperature for an hour (no change) counted as a dead sensor.- The "reaches the target within 6 runs" hold is replaced by a trend over the sensor's reports of the last hour: the setpoint is kept while the room moves towards the target and reaches it within
trend_hold_hours(default 2). Before, the change since the last run (12 minutes, often 0 or one 0.1 step) was used. - A learned setpoint is approached by at most 0.5 per run (or the normal adjustment, if larger). Before, the setpoint jumped to it (for example from 21.0 to 25.0 in one run).
- HVAC: nothing is learned within 2 hours of a mode change or while the unit reports an
hvac_actionother thanheating/idle, so heat /fan_onlycycling no longer teaches too high setpoints. - HVAC: the setpoint in heat is never set below
target_indoor_temp - max_setpoint_drop(default 3 C / 5.4 F). Back fromfan_only, the setpoint starts from the last one held in heat, at most the learned setpoint minus 1 (before: always the learned setpoint minus 1). See Heat pump cycling. - When a vacation ends with the room at least
cold_start_deficit(default 2 C / 3.6 F) below the target, the heating starts with a higher setpoint and comes down once the room is within 1 degree of the target, see Cold start after vacation. Before, the setpoint went from the vacation temperature to the setpoint plus the whole difference and then on up and down by the normal rules.
- The learned setpoints are no longer updated on vacation, with open windows, during
- Upgrading, indoor mode (classic): a vacation end heats with a higher start setpoint (cold start). Nothing to configure;
cold_start_deficitchanges when the cold start is used. - Upgrading, indoor mode (classic and adaptive): the limit of 0.5 per run (classic) and 0.5 per change (adaptive) is gone again. The setpoint goes to the computed value at once, so the start or end of
daytime_savings/daytime_increasingor a new target moves it in one run. Example: target 22.6, room 21.75, setpoint 21.5, adaptive controller calculates 24.0: 24.0 is sent, not 22.0. The adaptive controller still sends at most one change per 12 minutes. The only limit left is towards a learned setpoint (classic): at most 0.5 per run, see the step rules. Nothing to configure. - Upgrading, HVAC in heat (indoor mode): without
has_fireplacean HVAC now lowers its setpoint before it switches tofan_only(it switches at once only when the setpoint is at its lowest or the room still rises; a sensor that reports less often than about every 25 minutes does not show the rise, see Sensors that report rarely). A room with a fireplace, or another heat source that makes the room overshoot fast, setsoptions: [has_fireplace]to keep the old behaviour (fan_onlyat once at target + 0.6). - Upgrading,
controller: adaptivein indoor mode: the device now runs when the room sensor reports, not only every 30 minutes, see When a device runs. Nothing to configure. The interval is now the longest time without a run. Expect more runs and about the same number of setpoint changes. - Upgrading,
controller: adaptive/shadow: what the controller learns (the window level without sun, the offset of the climate's own sensor, the offset of the backup sensor) goes by time, also with the default interval of 30 minutes: it learns as fast as it did at 12 minutes (before, about 2.5 times slower at 30 minutes, and only one learning sample per interval even when the sensor reports started runs in between). Nothing changes withinterval_minutes: 12. Below 12 minutes the backup offset now follows as fast as at 12 (before, faster). The sample counts in the JSON file may have decimals. Nothing to configure, see What it learns. - Upgrading, every controller: a device also runs 5 seconds after every start and stop of a
daytime_savings/daytime_increasingperiod, and when a person in a period'spresencelist comes home or leaves. Classic keeps its fixed interval and does not run on sensor reports. daytime_increasingis +0.3 (was +0.5), hard-coded, in classic and adaptive (and -0.3 when cooling). The 0.5 held the room too warm after the period.daytime_savingsstays 0.5. When adaytime_savingsperiod is active at the same time as adaytime_increasingone, the savings replace the increase (the target goes from +0.3 straight to -0.5; before, the two were added). Nothing to configure; a room with an increase now ends about 0.2 C lower than before.- Hold after an increase fixed and configurable: the hold after a
daytime_increasingperiod in which HVAC does not go from heat tofan_onlywas one hour and was lost at midnight (a period ending before midnight, asked after it, counted as ended the evening before). It now works across midnight, andincrease_hold_hours(default 2 hours, 0 to 12) sets its length: more for a well insulated house, fewer for a leaky one. A period that ends shortly before adaytime_savingsperiod starts is the case it is for. Withcontroller: adaptivethe hold ends early on a large sun term or a room 1 C above thefan_onlyline, see After an increase. The hold now counts from the moment the increase stops applying: itsstop, or earlier when itsmodeorpresenceends it (a period withstop: '23:00:00'and the house going tonightat 21:40 holds from 21:40 to 23:40), or when adaytime_savingsperiod starts and replaces it (an increase 22:00-01:00 with savings from 00:00 holds from 00:00 to 02:00; the savings setpoint is sent at 00:00, only the switch tofan_onlywaits for the hold). It follows only a period that applied, and an HVAC infan_onlyat that moment gets no hold. Before, everystopstarted a hold, also on a day the period never applied, and a period ending with its mode had none. Upgrading: the default hold is now 2 hours instead of 1; setincrease_hold_hours: 1for the old length. - New, optional:
modeindaytime_savings/daytime_increasingentries and the keymode_app, start the savings from the house mode (night,away) of Lightwand / ModeManagement, see House modes in periods. Without amodekey nothing changes and nothing is listened to. New filecc_modes.pyin the app folder. - New, optional keys
interval_minutes(time between runs, default 30 / 15 minutes; it was 12 minutes for indoor mode before this version,interval_minutes: 12gives that back),min_run_gap_minutes(default 5),room_sensor_stale_minutes,backup_sensor_stale_minutes,window_sensor_stale_minutes,window_trigger_delta,trend_hold_hours(default 2),max_setpoint_drop(HVAC, default 3 C / 5.4 F),cold_start_deficit(default 2 C / 3.6 F),increase_hold_hours(default 2),mode_appand the optionhas_fireplace, see Device keys. - Window notifications are sent once per event: "Window open" (getting cold) once after windows are opened, "Window closed" (getting hot,
getting_hot_offsetabove target) once after they are closed. Before, "Window open" was sent once per app start and "Window closed" never. HVAC now uses the same rule as heaters: "Window open" also needs the outdoor temperature to be belowgetting_cold. - Outdoor values are kept per app, not shared between all ClimateCommander apps (they used to overwrite each other). At startup the last values sent by the Weather app are used.
- The periodic runs of the devices (every 30 minutes, 15 minutes in table mode, or
interval_minutes) are spread over the interval, so many devices do not call Home Assistant in the same second. outside_temperature,rain_sensorandanemometer(deprecated) are read inHASS_namespace.
Before:
climate_control:
module: climateCommander
class: Climate
json_path: /conf/persistent/Climate/
vacation: input_boolean.vacation
HVAC:
- climate: climate.living_room
indoor_sensor_temp: sensor.living_room_temperature
target_indoor_temp: 22.8After:
climate_living_room:
module: climateCommander
class: Climate
climate: climate.living_room
type: hvac
json_path: /conf/persistent/Climate/
vacation: input_boolean.vacation
indoor_sensor_temp: sensor.living_room_temperature
target_indoor_temp: 22.8- App-level keys (
vacation,vacation_temp,notify_app,notify_receiver,json_path,cloud_cover, ...) go into each app. YAML anchors (<<: *climate_defaults) can share them inside one file. - Learned setpoints: on its first start a one-device app copies the data for its entity from the other JSON files in
json_path(the old app's file), then writes its own<app name>.json. Remove the device from the old app when you move it. - Many apps: AppDaemon gives every app its own thread. With more than 20-30 apps, set
total_threads(for example 10) inappdaemon.yaml; the apps then share the threads.
Nothing to do for the default case: without controller, every device keeps the classic controller and behaves exactly as before. To try the new one:
- Add
controller: shadowto one device (and optionallydiagnostic_sensor). Classic stays in control; nothing else changes. Leave it for 1 to 2 weeks and read the DEBUG line or the diagnostic sensor. - Then change it to
controller: adaptive. - To go back, set
controller: classic(or remove the key). The learned setpoints of classic ("data"in the JSON file) are not touched byshadoworadaptive, and classic ignores the new"adaptive"entry, so you can switch back and forth.
- The ad-Weather app is required for outdoor temperature, rain, wind and lux. An ERROR is logged at startup if it is not found;
weather_appsets its name. Nothing to do if you already use it. Install ad-Weather 0.2.0 or later: it sends values it does not have asNone, which this version handles. outside_temperature,rain_sensorandanemometerare deprecated (a WARNING is logged). They still work and take precedence while they report.- Own outdoor lux sensors (
OutLux_sensor,OutLuxMQTT, and the_2versions) are used while they report; after 20 minutes of silence the lux from ad-Weather is used. The logic is in the new fileweather_data.py(a copy of the file in Lightwand), so copy the whole app folder when you update.
- Spelling correction:
notify_recieveris nownotify_receiver.
- MQTT default namespace: changed to
mqtt, the default in the AppDaemon documentation.
- Notifications: recipients are now sent as a list (one call). Custom notification apps may need updates.
- Weather sensors: automatic weather sensor detection was removed. Use the ad-Weather app or a custom event-based solution.
- Unified control naming: climate entities are configured as either HVAC or Heater.
indoor_tempwas renamed toindoor_sensor_temp.- The "vacation mode" (away state) was renamed to
vacation, to tell it apart from daily away states.
- Two ways to find the setpoint: keep a fixed indoor temperature from an indoor sensor (the setpoint that held the target is learned per outdoor temperature and lux), or follow a table of outdoor temperatures (floor heating, panel heaters).
- HVAC mode switching between
heat,coolandfan_only, the fan modeSilencein set periods, and theboostpreset in wind. - Screens / covers close when the room is warm and it is bright, and open on low lux, rain or wind.
- Vacation: a lower temperature, optionally the heater turned off (table mode).
- Open windows: lower temperature (heaters) or
fan_only(HVAC), and a notification when it gets cold or hot. - Time periods:
daytime_savings(lower),daytime_increasing(higher) andsilence(HVAC fan), optionally only depending on who is home. - Rain and wind raise the heating a little.
- Electricity price control with ElectricalManagement: save when electricity is expensive, spend before it gets expensive or on solar overproduction, reduce when the house is over its kWh goal.
- Home Assistant and AppDaemon 4.5, with Python 3.10 or later.
- The Python package
pydanticversion 2 (AppDaemon 4.5 depends on it, so it is normally installed already). - The ad-Weather app, version 0.2.0 or later. ClimateCommander looks for an app named
weather; setweather_appif yours has another name. An ERROR is logged at startup if it is not found. - All five files of the app in the same folder:
climateCommander.py,cc_devices.py,cc_config.py,cc_control.pyandweather_data.py. - A temperature sensor in the room (
indoor_sensor_temp). It is required in indoor mode, and it is used to find the unit (C or F) unless you setunit_of_measurement. - Optional: ElectricalManagement 1.0.5 or later for save / spend / reduce.
Clone the repository into your AppDaemon apps folder:
git clone https://github.com/Pythm/ad-ClimateCommander.git /path/to/appdaemon/apps/ad-ClimateCommanderUpdate with git pull in the clone. If you copy files by hand instead, copy all five (climateCommander.py, cc_devices.py, cc_config.py, cc_control.py, weather_data.py). An old climateCommander.py next to new helper files, or the other way round, does not work.
Add one app per climate entity to your AppDaemon apps.yaml. Keys you leave out use their defaults, see Configuration reference.
HVAC (heat pump) that keeps the room at 22.7 degrees:
climate_living_room:
module: climateCommander
class: Climate
climate: climate.living_room
type: hvac
indoor_sensor_temp: sensor.living_room_temperature
target_indoor_temp: 22.7
json_path: /conf/persistent/Climate/Floor heater that follows a table of outdoor temperatures:
heater_bathroom:
module: climateCommander
class: Climate
climate: climate.bathroom_floor
type: heater
indoor_sensor_temp: sensor.bathroom_temperature
target_indoor_temp: 24
target_heater_temp: 28
temperatures:
- out: -10
offset: 3
- out: 0
offset: 1
- out: 10
offset: 0
- out: 15
offset: -3json_pathis a folder and must end with/. The app writes<app name>.jsonthere. Create the folder first.- A table heater without
indoor_sensor_tempneedsunit_of_measurement: C(orF). Without a sensor and without that key the app cannot find the unit and stops with a WARNING.
Each climate entity gets its own app: top-level climate: (or heater:) and type:.
type |
Use for | What the app controls |
|---|---|---|
heater (default) |
floor heating, panel heaters, anything that only heats | the setpoint (and on/off on vacation with vacation_keep_off) |
hvac |
heat pumps, air conditioners | the setpoint, the modes heat, cool and fan_only, the fan mode Silence and the boost preset |
An unknown type logs a WARNING and the app uses heater.
Why one app per device: a change to one device's YAML reloads only that device, an error stops only that device, each app has its own small JSON file, and ElectricalManagement controls each device by app name. Every app listens to the WEATHER_CHANGE event itself.
Old style (deprecated): HVAC: and Heaters: lists of devices in one app still work and log a deprecation WARNING. In that style:
- the device keys go into each list entry, and the list decides the type (
HVAC:entries arehvac,Heaters:entries areheater); - these keys can also be given once at the app level and are then inherited by every device that does not set them itself:
vacation_temp,max_vacation_temp,screening_temp,getting_cold,getting_hot_offset,rain_level,anemometer_speed,spend_temp_offset,notify_receiver,cloud_cover; - a device can set its own
vacationentity; without it the app-levelvacationis used.
There are two target modes. The mode is chosen from the keys you use; set target_mode: indoor or target_mode: table to choose it yourself.
| Mode | Chosen when | Used for | Runs every |
|---|---|---|---|
indoor |
no temperatures, target_heater_temp or target_heater_input |
HVAC, heaters without a table | 30 minutes |
table |
temperatures, target_heater_temp or target_heater_input is set |
floor heating, panel heaters | 15 minutes |
interval_minutes (1 to 120) sets another time between the runs for one device, in both modes.
The first run is between 10 seconds and one interval after the app starts (spread per entity). Between the periodic runs a device also runs:
- 1 second after a window sensor has been
onfor 2 minutes, - 60 seconds after all window sensors are
off, - 5 seconds after the
vacationentity,target_indoor_inputortarget_heater_inputchanges, - at once when ElectricalManagement changes the power state,
- 5 seconds after every start and stop of a
daytime_savingsordaytime_increasingperiod, by the local clock, also on the days daylight saving time changes, - when a person in the
presencelist of such a period comes home or leaves (classic: only while that period applies by the clock), at most once permin_run_gap_minutes.
With controller: adaptive in indoor mode a device also runs on sensor reports, see When a device runs.
While the automate entity is off a device does nothing.
All the fixed steps below (0.3, 0.5, 1, 3, 4, 8 and so on) are in the unit of your sensors. They are not converted for Fahrenheit.
controller: adaptive in indoor mode does not wait for the interval:
- Every report from the room sensor (
indoor_sensor_temp) starts a run. - A change of at least
window_trigger_delta(default 1 C / 1.8 F) at the window temperature sensor (window_sensor_temp) since the last run starts a run, so sun is seen at once.0orfalseturns this off. This is the temperature sensor for the sun, not the open/closewindowsensors, which have their own rules above. - At most one of these runs per
min_run_gap_minutes(default 5). A report inside that time is not lost: one run follows at the end of it and reads the newest values. A sensor that reports every minute gives at most 288 runs a day, and a change is acted on after at most 5 minutes. - The interval (
interval_minutes, default 30) is now a safety net: the longest time without a run. It covers a silent sensor, the weather and a power state that expires. - The setpoint still changes at most once per 12 minutes. A change that has to wait gets its own run when the 12 minutes have passed.
- Reports from the backup sensor and the climate's own sensor never start a run. No report starts a run while a window is open or on vacation: the interval is enough there.
- What the controller learns, the diagnostic sensor and the JSON file go by time, not by the number of runs. A chatty sensor does not teach it faster or write more to Home Assistant than one write per 10 minutes plus the setpoint and mode changes. It learns as fast at the default interval of 30 minutes as at 12, also when the sensor reports rarely, see What it learns.
Example: a sensor with a 55 minute heartbeat gives a run at each report and one 30 minutes after it, about 50 to 60 runs a day. A sensor that reports every 10 minutes gives about 150.
These keep the fixed interval and do not run on sensor reports:
- Classic and shadow. Every classic run adds the whole difference to the setpoint, so more runs on a chatty sensor would mean more gain. Shadow runs classic, so it follows the same schedule, and what it proposes can come later than what
adaptivewould send. - Table mode, every controller. The setpoint comes from the outdoor table, and its changes have no minimum time between them.
After a time without a report a sensor counts as unavailable, and the normal fallback takes over: the backup sensor, then (adaptive) the estimate from the climate's own sensor.
| Key | Default | Applies to |
|---|---|---|
room_sensor_stale_minutes |
60 | indoor_sensor_temp |
backup_sensor_stale_minutes |
60 with adaptive and shadow, no limit with classic |
backup_indoor_sensor_temp |
window_sensor_stale_minutes |
720 with adaptive and shadow, no limit with classic |
window_sensor_temp |
Home Assistant sends no event when a sensor reports an unchanged value, so a heartbeat with the same value looks like silence to the app. A sensor with a 55 minute heartbeat can show 110 minutes without a report. With the default of 60 minutes that sensor is treated as offline now and then: the backup or the estimate is used for a while, with the WARNING no fresh room sensor.
Set the key to about 1.5 times the longest normal gap of that sensor: for the 110 minutes above, room_sensor_stale_minutes: 165. tools/ha_report_cadence.py in this repository finds the gaps in the sensor's history and suggests the value (python3 tools/ha_report_cadence.py sensor.x, read-only). The price of a higher value: a sensor that really stopped is believed for longer, and the controller keeps acting on its last value until then.
The time of a report is the latest of the entity's last_reported, last_updated and last_changed.
The room trend on such a sensor. A trend needs 3 reports inside its window. For an HVAC that window is about 52 minutes (60 in classic), so rate stays 0.0 on a sensor that reports less often than about every 25 minutes. A real sensor with a 55 minute heartbeat read 0.0 in about 99% of the runs. Two rules depend on it:
- "The room still rises 0.2 C (0.36 F) per hour" (heat to
fan_only, HVAC withouthas_fireplace). On such a sensor the rise is not seen: the setpoint is lowered instead, andfan_onlycomes only at the lowest setpoint. - The predictive trend (the proportional term acts on the room plus
ratetimes a short lead time). Only heaters use it. Their window is about 115 minutes, so for a heaterratestays 0.0 on a sensor that reports less often than about every 55 minutes.
For such a room with an extra heat source (fireplace, stove, strong sun through glass), use options: [has_fireplace] on an HVAC: it then switches to fan_only at target + 0.6 without needing the trend. The option does nothing on a heater.
To judge the trend yourself, read rate_wide on the diagnostic sensor (shadow and adaptive). It only shows the trend; the controller never uses it, and control keeps rate, because a wider window for control made normal rooms worse in simulation. Its window is 3 times the median of the last 9 gaps between reports of the room sensor, at least the normal window and at most 4 hours. With fewer than 3 reports spanning 15 minutes in that window it is empty (None), while rate shows 0.0, so empty means unknown and 0 means a flat room. On a sensor that reports often it equals rate. It uses the gaps the app has seen: a report with an unchanged value starts no run, so a frequent sensor whose value rarely changes can get a wider window too.
The target is target_indoor_temp, or the value of target_indoor_input (an input_number) when that is set. The app moves the climate's setpoint so that the indoor sensor reaches the target. Setpoints are rounded to 0.5.
Indoor temperature. The indoor_sensor_temp is used. When it is unavailable or has not reported for an hour (room_sensor_stale_minutes, see Sensors that report rarely), backup_indoor_sensor_temp is used. Without a backup the last value is used even if it is old. Without any value, nothing is done in that run.
Adjustments to the indoor value before the calculation (the setpoint is calculated as if the room had this temperature):
daytime_increasingactive: the room counts as 0.3 colder, so the room is held 0.3 warmer.daytime_savingsactive (also whiledaytime_increasingis, the increase then does not count): the room counts as 0.5 warmer, so the room is held 0.5 cooler. While daytime savings is active, rain and wind are not used.- Otherwise, wind at or above
anemometer_speedin the last 30 minutes, or (without wind) rain at or aboverain_level: the room counts as 0.3 colder.
Window sensor temperature. With window_sensor_temp (for example a sensor in a sunny window): when it is above target_indoor_temp + window_offset, the setpoint is lowered by the difference.
The room trend. The app keeps the reports of the indoor sensor in use (value and report time, from last_reported) for 3 hours: every report that changes the value, and at every run the latest report, also while an HVAC is in fan_only. The trend is the least-squares slope of the reports of the last 60 minutes, in degrees per hour. With fewer than 3 reports, or reports spanning less than 20 minutes, the trend is unknown; see Sensors that report rarely.
The step rules. The adjustment is the target minus the room temperature, plus the window part above.
- A small negative adjustment (between 0 and -0.3) becomes -0.5 when the setpoint has not changed for 30 minutes.
- A small positive adjustment (between 0 and 0.4) is ignored for 30 minutes after the last setpoint change.
- Trend hold: when the room moves towards the target and, at its trend, reaches it within
trend_hold_hours(default 2), the setpoint is kept. Below the target this means the setpoint is not raised, above the target that it is not lowered.- Example: target 23.0, room 22.5, reports from 22.1 fifty minutes ago up to 22.5 now. That is 0.48 per hour, so the target is reached in 0.5 / 0.48 = 1.04 hours: the setpoint is kept. At 0.1 per hour it would take 5 hours, so the setpoint is raised by the rules below. The same applies 0.5 above the target with the room falling 0.6 per hour (0.83 hours): the setpoint is not lowered.
- When the room moves towards the target more slowly than that (with an unknown trend: when it moved towards the target since the last run), or the trend moves away from the target, the learned setpoint does not count as valid in that run.
- Room temperature falling since the last run:
- if the setpoint is 4 or more below the target, the room is below the target and the setpoint has not changed for 4 hours, the new setpoint is the setpoint plus the adjustment minus 1;
- otherwise the adjustment is added, but only if the windows were closed more than 1 hour ago or the room is more than 1 below the target. The result is not set below a valid learned setpoint; when that learned setpoint is above the current setpoint, the setpoint moves up towards it by at most 0.5 per run (or by the adjustment, if that is larger). Example: setpoint 21.0, learned 25.3, room 0.2 below the target: 21.5, then 22.0 at the next run, and so on.
- Room temperature rising since the last run: the adjustment is added. The result is not set above a valid learned setpoint; when that learned setpoint is below the current setpoint, the setpoint moves down towards it by at most 0.5 per run.
- Room temperature unchanged: when the room is 0.3 or more away from the target and the setpoint has not changed for 30 minutes, the adjustment is added.
- Limits: the setpoint stays within 4 of a valid learned setpoint. Without a valid learned setpoint it stays within 8 of the target. HVAC in heat: never below
target_indoor_temp - max_setpoint_drop(default 3 C / 5.4 F) or the climate'smin_temp, see Heat pump cycling. - Sent at once: the result of the rules above is sent in that run, rounded to 0.5 (heat and cool). Example: setpoint 21.0, the rules give 22.8: 23.0 is sent. The only limit per run is the one towards a valid learned setpoint described above.
Learning (needs json_path). When the setpoint has not changed for 2 hours (and at most once every 2 hours), the setpoint is stored as a running average for the current outdoor temperature (2-degree steps) and outdoor lux (5000-lux steps). A learned setpoint counts as valid when it exists for the current outdoor temperature and lux step and has at least 10 samples. After 100 samples the count is set back to 10, so recent samples weigh more. Learning happens only in normal operation: not with open windows, not on vacation, not during daytime_savings or daytime_increasing, not while ElectricalManagement sends save, spend or reduce, and not while an HVAC unit cools. An HVAC also learns nothing within 2 hours of a mode change (heat, fan_only, cool, off; by the app or by hand), and nothing while the climate reports an hvac_action other than heating or idle. Without json_path nothing is learned and the current setpoint is the reference.
Open windows (any windowsensors entity on for 2 minutes): the heater setpoint becomes vacation_temp, or save_temp when that is set and lower (the lowest of the two, as in table mode). HVAC switches to fan_only instead, see HVAC modes.
Vacation (vacation entity on): the setpoint becomes vacation_temp (default 16 C / 61 F). When it ends, see Cold start after vacation.
Power states from ElectricalManagement: see ElectricalManagement. Open windows and vacation win over the power states.
Indoor mode, heaters and HVAC in heat. Only when the vacation entity goes from on to off (not after an open window, not on a cold morning). The first run after that (5 seconds later) compares the room (the same indoor value as in every run, with the daytime, wind and rain adjustments) with the target:
- The room is at least
cold_start_deficitbelow the target (default 2 C / 3.6 F): the setpoint becomes the target plus that difference, at most 6 C (10.8 F) above the target and at most the climate'smax_temp, sent at once. Examples, target 23: room 17 gives 23 + 6 = 29 (ormax_temp, if lower); room 20.5 gives 23 + 2.5 = 25.5. An HVAC that was infan_onlyswitches to heat first. - It is held while the room is more than 1 C (1.8 F) below the target. Nothing is learned meanwhile, and no other step rule moves it. A power state from ElectricalManagement works on top of it as usual (the start setpoint is the base).
- The way down: once the room is within 1 C of the target (example: 22.0 or warmer with target 23), or 8 hours after the vacation ended, the setpoint goes in one move to the setpoint held before the vacation or the learned setpoint, whichever is lower (the one that exists if only one does; without both the target + 1 C), never below the lowest heat setpoint of an HVAC and never above
max_temp. Example: target 23, room 17, learned setpoint 24.5: 29 until the room is at 22.0, then 24.5, then the normal rules. - The room is less than
cold_start_deficitbelow the target: no cold start. The setpoint becomes the one held before the vacation, at most the learned setpoint minus 1 (without learned data the target minus 1), for an HVAC not below its lowest heat setpoint (as afterfan_only). Example: target 23, room 21.5, learned 25.5, held before the vacation 23.5: 23.5. - The cold start ends early when a window opens, a new vacation starts, an HVAC leaves heat (
fan_only, cool, off; a heater that is turned off), or the app restarts (it is kept in memory only); the normal rules continue from the setpoint at that moment. - With
controller: shadowthe classic controller does this (it stays in control); withcontroller: adaptivethe controller does the same with its own values, see What differs from classic.
The table needs the outdoor temperature. After a start the device waits for the first outdoor temperature from the Weather app (or the deprecated outside_temperature sensor). A heater that is off, unavailable or unknown is left alone.
Base setpoint. The table entry for the outdoor temperature is the last entry whose out is at or below the outdoor temperature (an entry applies from its out value and up). Below the first entry, the first entry is used. The table is sorted by out for you.
- Entry with
offset: base =target_heater_temp(or the value oftarget_heater_input) plusoffset. - Entry with
normal(and nooffset): base =normal. - No table at all: base =
target_heater_temp.
Without target_heater_temp and target_heater_input, the indoor target is used as the heater target: target_indoor_temp, or target_indoor_input as the heater input (as in ElectricalManagement).
Corrections, in this order:
- Rain at or above
rain_level, or wind at or aboveanemometer_speedright now: +1. window_sensor_tempabovetarget_indoor_temp + window_offset: lower by the difference.- Indoor temperature above
target_indoor_temp: lower by the difference as well. Window and indoor together lower the setpoint by at most 3 degrees, also when only the window sensor is above its limit (a sunny window can no longer drop the heater to its floor). The indoor temperature isindoor_sensor_temp(with the backup rules above), or the climate's owncurrent_temperatureattribute when there is no indoor sensor. - Vacation: the setpoint becomes the vacation temperature (see below).
- ElectricalManagement
save, ordaytime_savingsactive: the save temperature (see below). - ElectricalManagement
spend: +spend_temp_offset(default 1). This is added also on vacation and during daytime savings. - Never below the vacation temperature, and never below the climate's
min_tempattribute (5 if the climate has none).
Save temperature, the first rule that applies:
save_temp_offsetis set: the setpoint plussave_temp_offset.save_tempis set:save_tempplus the entry'soffset, or the setpoint if that is lower.- The entry has
save: the entry'ssave, or the setpoint if that is lower. - Otherwise: the climate's
min_temp(5 if it has none).
Vacation temperature, the first rule that applies (calculated from the base setpoint):
vacation_tempis set:vacation_tempplus the entry'soffset, or the base setpoint if that is lower.- The entry has
vacation: the entry'svacation, or the base setpoint if that is lower. - Otherwise: the climate's
min_temp(5 if it has none).
Open windows and ElectricalManagement reduce: the setpoint becomes the lowest of the save temperature and the vacation temperature (both calculated from the base setpoint), because that stops the heater drawing power fastest. It is sent only while the climate is in heat or cool and its setpoint is above the save temperature. If the climate has no setpoint, its min_temp is sent.
Vacation with vacation_keep_off: true (or options: [vacation_keep_off]): the heater is turned off instead (up to 4 attempts per vacation). When the vacation entity turns off and the heater is off, the app turns it on in heat mode. This happens in table mode also without vacation_keep_off.
Not used in table mode: learning, the step rules and daytime_increasing.
HVAC can use table mode too. Then the heat setpoint comes from the table, while cooling always uses the indoor rules.
A table can use offsets or absolute temperatures per entry:
temperatures:
- out: -4
normal: 20
save: 13
vacation: 14
- out: 6
normal: 18
save: 13
vacation: 14Each period is a list entry with start, stop and optionally presence (a list of person entities). Write the times in quotes ('10:00:00'); without quotes YAML can read them as a number, which fails validation.
| Key | Active when | Effect |
|---|---|---|
daytime_savings |
between start and stop, and nobody in presence is home. An entry with only presence (no times) is active whenever nobody in it is home. |
Indoor mode: room held 0.5 cooler when heating, 0.5 warmer when cooling. Table mode: the save temperature. Savings that start while a daytime_increasing period applies replace it and start its hold: the savings setpoint is sent at once, but an HVAC only switches from heat to fan_only when the hold ends, see After an increase. |
daytime_increasing |
between start and stop, and (with presence) someone in presence is home |
Indoor mode: room held 0.3 warmer when heating; 0.3 cooler when cooling. Ignored in table mode. During increase_hold_hours after a period that applied stops applying (at its stop, or earlier when its mode or presence ends it or daytime_savings replace it; default 2 hours, also past midnight; none for an HVAC in fan_only at that moment), HVAC does not switch from heat to fan_only, see After an increase. |
silence (HVAC) |
between start and stop, not on vacation, and (with presence) someone in presence is home. Only the first period that matches the time is checked. |
Fan mode Silence, see HVAC modes. |
A device runs 5 seconds after every start and stop of daytime_savings and daytime_increasing, so a period acts at its time and not at the next run. It also runs when a person in their presence lists comes home or leaves. silence only sets the fan mode and has its own timers at its times; it starts no run, and its presence is read at the next run.
After a daytime_increasing period the room is warmer than the target, and the target then drops (the increase ends, and a daytime_savings period may start right after it). The unit would see a room above the fan_only line and switch to fan_only, and the room would fall below the target before it heats again. So for increase_hold_hours (default 2) after the increase ends an HVAC does not switch from heat to fan_only; the savings start at their own time, only the fan_only switch waits. The hold counts from the moment the increase stops applying: the stop time when the period runs to it, also when the check is made after midnight (a period ending 23:30 holds until 01:30 with the default; before this version the hold was lost at midnight). A better insulated house cools more slowly and may need more hours, a leaky one can set fewer.
The hold follows only a period that applied, because only then is the room warmer than the target. A period applies while its times hold, its mode matches the house mode and (with presence) someone in the list is home. When it stops applying before its stop (the house mode changes, or the last person in presence leaves), the hold counts from that moment: a period active in mode day with stop: '23:00:00' and the house going to night at 21:40 holds from 21:40 to 23:40, and not after 23:00. The moment is the MODE_CHANGE event or the person leaving (or the first run after it, when the change is only seen there). A period with a mode and no stop holds from the mode change that ends it. When the period applies again (the person comes back), the earlier hold ends, and a new one starts when it stops again. A period whose mode never matched, or with nobody in presence home, has no hold that day.
When a daytime_savings period starts while the increase applies, the savings replace the increase, and that counts as the increase stopping: the hold counts from the savings start, their start time or the MODE_CHANGE event or person leaving that starts them (an increase 22:00-01:00 with savings from 00:00 holds from 00:00 to 02:00, not from 01:00). The hold wins over the savings: their setpoint is sent at their start, but an HVAC stays in heat until the hold ends (with has_fireplace, fan_only comes 2 hours after the savings start instead of at once; adaptive still ends the hold early, see below, classic does not). Heaters are not affected. Savings that start while no increase applies, or that are already on when its times begin, start no hold. When the savings end while the increase's times still run, the increase applies again and the hold ends; a new one starts when it stops again (at its stop).
An HVAC that is in fan_only when the increase stops applying gets no hold: it is not heating, and a hold would only keep it from fan_only later for no reason. Heaters, and an HVAC in heat or cool at that moment, get the hold.
After a restart in the middle of a period, the first run checks the same conditions. After a restart inside the hold the app has not seen the period: for a stop before the restart, the mode and presence at the restart decide (hold from that stop); a hold that a mode change, a person leaving or a savings start began before the restart is lost (restarted while savings have replaced the increase: no hold, also not after its stop; restarted after its stop: hold from the stop).
With controller: adaptive the hold ends early, so the fireplace and the sun still work:
- the sun term of the window sensor is 1 C (1.8 F) or more (the same limit that keeps
fan_onlyfrom going back to heat), or - the room is more than 1 C (1.8 F) above the normal
fan_onlyline (target + 0.6), for example with a fireplace lit.
Open windows (windowsensors) switch to fan_only at once, whatever the hold says. The classic controller has the hold but not the early end. The device runs again when the hold ends (adaptive), so fan_only is not left waiting for the next sensor report. The diagnostic sensor shows increase_hold: true while a switch to fan_only is waiting.
An entry in daytime_savings or daytime_increasing can name a house mode (a word or a list) that it applies in. The mode is the one from the MODE_CHANGE event that Lightwand and ModeManagement use, so a house that already has them can start the savings with night or away:
mode_app: manageModes # optional: ModeManagement's app name, read at startup
daytime_savings:
- mode: night
stop: '05:00:00' # heat up again before the morning
- mode: away
stop: '15:30:00' # and before anyone comes home- The entry applies while the house mode is one of the names.
nightalso matches every mode that starts with the night word (night,nightKid, ...). The names are Lightwand's mode keys (automagical,morning,night,away,off, ...) and are translated if Lightwand's language file is installed (nightin the YAML matchesnattin a Norwegian house); any other text is compared as written, for examplefamily. - Times are optional. With
startandstopit applies in that window while the mode is on. With onlystopthe window starts 12 hours before it; with onlystartit ends 12 hours after it. With no times it lasts as long as the mode.stopis how you give the room time to heat up again: the savings end atstopeven if the mode is stillnightoraway, and the target goes back to normal. - When the mode changes the device runs 5 seconds later, so the savings start or end at once (someone coming home ends
awaysavings, the morning mode endsnightsavings). presencestill works in the same entry: the mode and the person rule must both apply.- The house mode is learned from
MODE_CHANGEevents.mode_app(optional, the name of the ModeManagement app) also gives it after a restart; without it the mode is unknown until the next mode change, and an unknown mode matches nothing. Amode_appname that does not exist logs one WARNING. - Nothing is needed from Lightwand or ModeManagement unless an entry has a
mode: without it no listener is created and nothing changes. Do not putmode_translationor the ModeManagement app underdependencies:unless they exist (a name that does not exist stops apps from starting);mode_appis read when needed and needs no dependency. - A
daytime_increasingperiod that ends with its mode (nostop) holds from the mode change, see After an increase.
Everything above describes the classic controller, the default. With controller: adaptive a device finds its setpoint in a different way; with controller: shadow it only calculates what the adaptive controller would do and changes nothing. The key is per device:
controller |
What happens |
|---|---|
classic (default) |
As described in How the setpoint is found. Nothing new. |
shadow |
Classic stays in control and sends exactly what it sent before. The adaptive controller runs next to it on its own proposals, writes one DEBUG line per run and the optional diagnostic_sensor. Use it first. |
adaptive |
The adaptive controller sets the temperature (and for HVAC the mode heat / cool / fan_only and the boost preset). |
It works in indoor mode for heaters and HVAC, and in table mode for heaters. An HVAC in table mode keeps the classic controller (one INFO line).
Every run (see When a device runs) the setpoint is calculated again from these parts:
- The target:
target_indoor_temportarget_indoor_input, ortarget_table(below). On top, the same shifts as classic, now applied to the target: -0.5 duringdaytime_savings(it replaces an increase); otherwise +0.3 duringdaytime_increasingand +0.3 with wind or rain. - A feed-forward from the climate's own sensor: a heat pump regulates its own sensor (
current_temperature), which often reads 2 to 5 degrees above the room. The controller learns that offset (see below) and, for HVAC, adds half of it, so the setpoint moves with it before the room drifts. Heaters do not use it.outdoor_gainadds a fixed extra per degree that the slow outdoor temperature is below the target. - A slow integral term, which finds the setpoint that holds the room. It only runs while the room is near the target, outside a small band (room more than 0.1 above or 0.25 below the target, as the classic asymmetry), and it is frozen while a window is open or closed less than an hour ago, on vacation, in a power state, without a room sensor and (HVAC) while the sun term is large.
- A proportional term on the filtered room temperature (heaters: plus
ratetimes a short lead time, see Sensors that report rarely). - Minus a sun term: how far the window sensor reads above its learned level without sun. It rises at once and fades over about two hours (walls and floor keep the heat), times
solar_gain, at mostsolar_max.
The result stays within the target Β± 8 (an HVAC in heat at most max_setpoint_drop below the target, default 3 C / 5.4 F), the climate's min_temp and max_temp attributes, and never below the vacation temperature. It is rounded to 0.5 and sent at most once per 12 minutes, the whole change at once, when it is about 0.5 or more away from the current setpoint. Window, vacation and the start or end of a power state are sent at once. A setpoint changed by hand (or by another app) is followed: the controller continues from it.
Start and restart. At the first run after a start or a reload the controller takes over the climate's current setpoint, so nothing jumps. If the room is then far from the target (more than about 1.4 degrees for HVAC, 3.9 for heaters) it does not wait: the integral starts at 0 and the calculated value is sent at once. A setpoint changed by hand is followed in the same way as in classic. When the HVAC mode changes, the Silence fan mode (if a silence period is active) is set in a separate command a few seconds later, because a mode and a fan mode sent together can lose one of them.
On top, unchanged from classic: open windows (heater: vacation_temp, HVAC: fan_only), vacation, and the power states from ElectricalManagement (the setpoint at the start of the state plus the same offsets). New: the comfort guard drops the power-state offset until the state changes when the room is comfort_guard below the target in save / reduce, or that much above it in spend.
What it learns (stored per climate under "adaptive" in the JSON file, see JSON):
- The window level without sun: window sensor minus room sensor, by outdoor temperature, with extra terms for wind and rain. It is learned only on quiet nights: room sensor fresh, windows closed for an hour, no vacation or power state, lux below 500 and no sun for an hour. It may be negative (a cold window) or positive (a sensor above a heater or in the air flow of a heat pump). Until enough is learned,
window_offset(default -3 C / -5.4 F) orwindow_offset_tableis used. - The offset of the climate's own sensor:
current_temperatureminus the room, by the slow outdoor temperature, only inheatwith a settled room. - The offset of the backup sensor:
indoor_sensor_tempminusbackup_indoor_sensor_tempwhile both report, a running average that follows a change by 63 % in about 2 hours. It is added to the backup while the indoor sensor is missing. - Learning goes by time. The window level and the climate-sensor offset take at most one sample per 12 minutes. With an interval longer than 12 minutes a sample counts for the time since the previous one (at most one interval), so the default of 30 minutes learns as fast as 12: about an hour of quiet night before a learned window level is used, about 6 hours (heaters: 2) before it weighs as much as
window_offset. Extra runs on sensor reports do not make it faster.
Without a room sensor (unavailable, or not reported within room_sensor_stale_minutes, default an hour, and no usable backup): the room is estimated as the climate's own sensor minus the learned offset (or internal_offset_table). The integral is frozen, nothing is learned and an HVAC does not change mode on an estimate. Without any offset the controller runs open loop on the feed-forward. One WARNING when this starts, one INFO when the room sensor is back. Classic does nothing in indoor mode in that case.
Table-mode heaters keep the table: base from temperatures, +1 for rain or wind, then the sun term and "room above target" together lower it by at most 3, and save / spend / vacation / min_temp as classic. Differences: the room is the room sensor or the estimate above; without either, the "room above target" part is left out (classic uses the thermostat's current_temperature, often the floor, as the room and lowers the setpoint by up to 3). A small trim (at most Β±2) moves the table value towards target_indoor_temp while a real room sensor reports.
- Set
controller: shadow, optionallydiagnostic_sensor: sensor.<room>_controller, and leave it for 1 to 2 weeks. Ajson_pathkeeps what it learns over restarts. - Read what it would do. Either set
log_level: DEBUGfor the app inapps.yaml; each run then logs one line such as
climate.living_room: shadow heat src primary R 21.9 in_f 21.93 rate 0.1 rate_wide 0.1 out_slow 3.4 out_day 4.1 excess -0.3 base_w -2.5 solar 0.0 bias_int 2.1 target_eff 22.0 ff 1.05 i -0.32 p 0.07 sp_ctrl 22.3 send None hold 22.5 frozen None classic 22.0 heat
or open the history of the diagnostic sensor in Home Assistant. Its state is the setpoint the controller would hold;classic_setpointis what classic did. - Check that
srcisprimarymost of the time, thatbase_w(the learned window level) looks plausible for the room, and thatsp_ctrlandclassic_setpointare not far apart for long. - Then set
controller: adaptive.
Heaters: adaptive works for heaters too, but the heater constants were only tuned in a simulation with guessed floor-heating physics. Always run a heater in shadow first.
| Diagnostic attribute | Meaning |
|---|---|
| state | Setpoint the controller holds (shadow: would hold) |
src, R |
Room value used: primary, backup (with a learned offset), estimate (climate sensor minus offset) or none |
in_f, rate |
Filtered room temperature and its trend per hour. rate stays 0.0 on a sensor that reports less often than about every 25 minutes (heater: 55) |
rate_wide |
Read-only: the room trend per hour over a window that follows the sensor's report gaps, for you to judge the trend. The controller never uses it. None (and left out of the DEBUG line) with too few reports. See Sensors that report rarely |
out_slow, out_day |
Outdoor temperature filtered over 8 to 9 hours (the walls) and over 24 hours |
excess, base_w, solar |
Window sensor above its learned level (base_w), and the sun term |
bias_int |
Learned offset of the climate's own sensor |
target_eff, ff, i, p, sp_ctrl |
Effective target, feed-forward, integral, proportional term, unrounded setpoint |
trim, room_term |
Table mode: trim and the "room above target" part |
send, frozen |
What was sent this run (shadow: would be sent), and why the integral is frozen |
classic_setpoint, classic_mode |
shadow only: what classic set |
The diagnostic sensor is written with set_state by the runs: at once when the setpoint or the mode changes, otherwise at most every 10 minutes. Home Assistant does not keep such an entity over its own restart; it comes back at the next run (within one run, 30 minutes by default). Its history is recorded like any other entity.
All optional. Only shadow and adaptive read them; with classic they are ignored with one INFO line.
| Key | Type | Default | Meaning |
|---|---|---|---|
controller |
string | classic |
classic, shadow or adaptive. |
diagnostic_sensor |
entity | none | For example sensor.living_room_controller. Written by the runs, at most every 10 minutes while the setpoint and mode hold, see above. |
window_offset |
number | -3 | Classic: unchanged (the window sensor lowers the setpoint above target + window_offset). Adaptive: only the starting value of the learned window level, relative to the room. When not set, adaptive uses -3 C / -5.4 F. |
window_offset_table |
list | none | Starting values of the window level by outdoor temperature (see below). Needs window_sensor_temp. |
internal_offset_table |
list | none | Starting values of the climate's sensor minus the room by outdoor temperature. Gives an estimate without a room sensor from day one; the only source for a heater with no room sensor at all. |
target_table |
list | none | Indoor target by the 24-hour mean outdoor temperature (see below). |
outdoor_gain |
number 0 to 0.6 | not set | Fixed extra feed-forward per degree the slow outdoor temperature is below the target. Not set: only the learned offset is used. |
solar_gain |
number 0 to 5 | 1.0 | Multiplies the sun term. |
solar_max |
number above 0 | 3 C / 5.4 F | Largest sun term. |
comfort_guard |
number above 0 | 1.5 C / 2.7 F | Power-state offset dropped (until the state changes) when the room is this far below the target in save / reduce, or above it in spend. |
window_trigger_delta |
number 0 to 20, or false |
1 C / 1.8 F | adaptive in indoor mode: a run when window_sensor_temp has moved this far since the last run. 0 or false: off. See When a device runs. |
Fixed, not keys: one change per 12 minutes, JSON written at most every 30 minutes, the diagnostic sensor at most every 10 minutes while the setpoint and mode hold. The stale limits are keys, see Sensors that report rarely: the room sensor after 1 hour, the window sensor after 12 hours (window sensors often report only on a 1-degree change).
Offset tables (window_offset_table, internal_offset_table). Each entry applies from its out and up, like temperatures. They seed the learning: with data the learned values take over step by step. Leave them out unless you know the values (for example from the diagnostic sensor after weeks of shadow).
| Entry key | Type | Required | Meaning |
|---|---|---|---|
out |
number | yes | Outdoor temperature from which the entry applies. |
offset |
number | yes | Window sensor (or the climate's sensor) minus the room sensor. Between -30 and 30. |
wind |
number | no | Extra while windy (wind at or above anemometer_speed in the last 30 minutes). |
rain |
number | no | Extra while it rains (at or above rain_level). |
Target table (target_table): each entry has out and either target (absolute indoor target) or offset (added to target_indoor_temp or target_indoor_input). The outdoor value is the 24-hour mean, so the target follows the season, not day and night, and it moves to another entry at most once a day. With a target entry, target_indoor_input has no effect while that entry applies.
Unknown keys in a table entry, the same out twice, or target and offset together are errors: the device is skipped with an ERROR naming the key.
climate_living_room:
module: climateCommander
class: Climate
climate: climate.living_room
type: hvac
json_path: /conf/persistent/Climate/
indoor_sensor_temp: sensor.living_room_temperature
window_sensor_temp: sensor.living_room_window_temperature
target_indoor_temp: 22
controller: shadow
diagnostic_sensor: sensor.living_room_controller
target_table: # optional
- out: -30
offset: 0.5
- out: 12
target: 21.5- No learned setpoints per outdoor temperature and lux: the
"data"entries in the JSON are neither read nor written. - No trend hold (
trend_hold_hoursis classic only), no steady-room correction, no extra -1 after 4 hours turned down, no fixed Β±4 around a learned value. - The trend uses the real room reading and the real time between reports. The daytime and wind / rain shifts move the target, not the reading.
- The window sensor counts relative to its learned level, not to
target + window_offset, and it is not added up run after run. - HVAC modes: heat to
fan_onlywhen the room is 0.6 above the effective target (plus the spend offset duringspend) and the setpoint is at its lowest (max_setpoint_dropbelow the target) or the room still rises 0.2 C (0.36 F) per hour (not seen on a sensor that reports less often than about every 25 minutes, see Sensors that report rarely); withoptions: [has_fireplace]at 0.6 above alone.fan_onlyto heat at or below the effective target, not while the sun term is large, starting half a degree below the calculated setpoint. The 0.6 hysteresis now holds during daytime periods too. At least 30 minutes in a mode before an automatic switch (window and vacation switch at once). No heat tofan_onlywithin 45 minutes of wind, in theincrease_hold_hoursafterdaytime_increasing(ends early with a large sun term or a room 1 C above the line, see After an increase) or in the hour after a power state ended. Cooling uses the classic thresholds. - The comfort guard during power states. With the defaults (
cold_start_deficit2,comfort_guard1.5) a cold start after a vacation (below) is far enough below the target for the guard, sosave/reducedo not lower it. - Cold start after vacation (indoor mode): when the vacation ends with the filtered room at least
cold_start_deficitbelow the effective target, the setpoint is the effective target plus the difference (at most 6 C / 10.8 F above, at mostmax_temp), sent at once and held with the integral frozen; once the room is within 1 C (1.8 F) of the effective target, or after 8 hours, one move to the controller's own calculated setpoint, with the integral set so that it stays there. Ends early on a window, a new vacation,fan_only/ cool, or a setpoint changed by hand; not kept over a restart. Inshadowthe controller only proposes it (virtual setpoint, diagnostic sensor); classic does its own cold start on the climate. - The room estimate from the climate's own sensor when the room sensor is gone.
- Indoor mode: the vacation temperature is also the lowest setpoint in normal operation, and the climate's
min_temp/max_tempattributes are limits. In F, make sure the climate reports them in F. - A heater whose climate is
offis left alone (classic sets the temperature anyway). - Without any lux source (lux stays 0), day and night from sunrise and sunset decide when the window level may be learned, until a lux value above 0 arrives.
- After a restart the state comes back from the JSON (written at most every 30 minutes, at once when a power state starts or ends, and when AppDaemon stops or reloads the app), and a running power state is not applied twice.
- The state is stored under
"adaptive"next to the classic"data"for each climate in<json_path>/<app name>.json. Classic never reads it. - Switching between
classic,shadowandadaptiveis safe in both directions. Whatshadowlearned is used byadaptive. Switch while ElectricalManagement sendsnormal: duringsave/spendthe new controller takes the setpoint that already holds the offset as its base, so the offset counts twice until that state ends. - A state saved in another unit (C / F) or by another version is ignored with one INFO line, and learning starts again.
- To reset only the adaptive learning: stop the app, delete the
"adaptive"entry of that climate, start the app. - Without
json_patheverything learned is lost at every restart (one INFO line).
An HVAC device runs as described in When a device runs and acts on the climate's current mode. It never turns an HVAC unit on from off (except in table mode when vacation ends, as for heaters). In a mode other than heat, cool, fan_only or off it logs a WARNING and does nothing. Without an indoor temperature it logs an INFO message and does nothing.
"Room" below is the indoor temperature after the daytime, rain and wind adjustments described above, except in fan_only, where the sensor value is used as it is.
In heat:
- Windows open: switch to
fan_only. - Vacation: setpoint
vacation_temp. When the room is abovetarget_indoor_temp - 3and abovevacation_temp + 3: switch tofan_only. - Otherwise the setpoint comes from the indoor rules (or the table), and:
- wind at or above
anemometer_speedin the last 25 minutes and the room more than 0.7 below the target: presetboost, if the climate offers it and the fan mode is notSilence. When that is no longer the case, aboostpreset is set back tonone; - room more than 0.6 above the target, no wind in the last 45 minutes, and not in the
increase_hold_hours(default 2) after adaytime_increasingperiod (After an increase):- with
options: [has_fireplace], in table mode, or while ElectricalManagement sendssave,spendorreduce: switch tofan_only; - otherwise switch to
fan_onlyonly when the setpoint is already at its lowest (target_indoor_temp - max_setpoint_drop, at least the climate'smin_temp) or the room still rises 0.2 C (0.36 F) per hour or more (not seen on a sensor that reports less often than about every 25 minutes, see Sensors that report rarely). Else the setpoint is lowered by the adjustment, at once and not below the lowest, and the unit stays in heat;
- with
- after such a lowering: once the room is at or below the target and does not reach it within
trend_hold_hoursby itself, the setpoint goes back in one move to the value from before the lowering, at most the learned setpoint and the target + 4.
- wind at or above
In cool:
- Windows open: switch to
fan_only. - Switch to
fan_onlywhen the room is less than 1 above the target, or, between 30 minutes before sunset and sunrise while it rains, when the room is less than 2 above the target. - Vacation: switch to
fan_onlywhen the room is belowmax_vacation_temp - 2. - Otherwise the setpoint follows the indoor rules (also in table mode). Cooling setpoints are not learned. The ElectricalManagement offsets change sign (save: +0.3 by default, spend: -1 by default).
- Rain and wind work in the same direction as in heating: the room counts as 0.3 colder, so it cools less.
In fan_only (not on vacation, windows closed):
- Switch to
coolwhen the room is more than 2 above the target, the outdoor temperature is abovetarget_indoor_temp - 4, and either it is between sunrise and 30 minutes before sunset without rain, or the room is more than 3 above the target. - Switch to
heatwhen the room is at or below the target. 10 seconds later the setpoint is set to the last setpoint the app held in heat, but at most the learned setpoint minus 1 (without learned data: the current setpoint minus 1, or the target minus 1 if the setpoint is more than 6 away from the target). After an app start or reload, before anything was held, the learned setpoint minus 1 is used. In indoor mode never below the lowest heat setpoint (target_indoor_temp - max_setpoint_drop). Right after a vacation ended, the heat run 10 seconds later sets the setpoint as described in Cold start after vacation instead.
In fan_only on vacation (windows closed):
- Switch to
heatwhen the room is at or belowtarget_indoor_temp - 5or at or belowvacation_temp + 2. - Switch to
coolwhen the room is abovemax_vacation_temp(default 30 C / 86 F).
Fan mode Silence: used only if the climate lists Silence in its fan_modes. In a silence period the fan mode is set to Silence; the previous fan mode is stored (in the JSON file when json_path is set) and set back when the period ends. If no previous fan mode is known, Auto is used. The fan mode is checked in every run, whatever the HVAC mode. A daily timer runs at every start and stop time of the silence periods, so the fan mode changes on time and not at the next run (the same check still runs every run and 5 seconds after a mode change, because a mode and a fan mode sent together can lose one of them). Times in silence must be quoted strings such as '21:00:00'.
A heat pump whose room sensor sits in the unit's air flow can cycle between heat and fan_only all night. Seen in a real bedroom (target 23, outdoor 3 to 5 C): the sensor rose about 1 degree in 25 minutes while the unit heated and fell 1 degree in 30 minutes in fan_only, so it swung between 22.5 and 23.9 with a mode switch about every 25 minutes. The evening before, at 6 to 9 C outside, setpoint 21.5 held the same room at 23.2 without a single switch. Three things kept the cycle going: each return from fan_only sent the learned setpoint minus 1 (24.5), the next run jumped to the learned 25.5, and the learned setpoints for 0 to 5 C outdoor had been pushed up to 24.5 to 25.5 by earlier cycles. The unit idles once its setpoint is at or below its own sensor reading (22 to 28 here, while the room is 23), so a much lower setpoint only makes the way back slow.
What the classic controller does about it (indoor mode, HVAC in heat):
- Trend hold over the reports of the last hour (see the step rules), kept up in
fan_onlytoo, so the first run afterfan_onlyalready knows that the room was falling. - A learned setpoint is a ceiling, not a jump target: the setpoint moves towards it by at most 0.5 per run. Back from
fan_only, the setpoint starts from the last one held in heat, at most the learned setpoint minus 1. - No learning within 2 hours of a mode change, so cycling does not push the learned setpoints up further.
- Lower before
fan_only(withouthas_fireplace): at target + 0.6 the unit switches tofan_onlyonly when the setpoint is already at its lowest or the room is still rising (not seen on a sensor that reports less often than about every 25 minutes, see Sensors that report rarely); otherwise the setpoint is lowered by the normal adjustment, at once, and the unit stays in heat. The lowest setpoint istarget_indoor_temp - max_setpoint_drop(default 3 C / 5.4 F). - Way back after a lowering: once the room is at or below the target again and does not reach it within
trend_hold_hoursby itself, a setpoint lowered this way goes back to the value from before the lowering, even when the adjustment alone would give less. Example: target 23,max_setpoint_drop3 (lowest 20), learned setpoint 25.5. The room is at 23.7 with setpoint 24.5 and no longer rising: instead offan_onlythe setpoint is lowered by the adjustment (24.5 - 0.7 = 23.8, sent as 24.0), and again at the next runs. Say it has reached 22.5 when the room is down at 22.9 and still falling: the next run sets it back to min(24.5 from before the lowering, learned 25.5, 23 + 4) = 24.5. This is the classic controller; the adaptive controller has no such rule, it sends its calculated setpoint.
options: [has_fireplace]: in a room with a fireplace (or another heat source that makes the room overshoot fast) waiting in heat is wrong. With this option the unit switches to fan_only at once at target + 0.6, as before 1.4.0. The other rules above still apply. It matters most with a room sensor that reports rarely, where the rise is not seen at all: see Sensors that report rarely.
interval_minutes: a room sensor in air flow reacts within minutes; the default of 30 minutes gives each setpoint change time to show before the next one. A shorter interval (12 was the default before this version) reacts faster but can chase the sensor. With controller: adaptive in indoor mode the runs follow the sensor at most once per min_run_gap_minutes, and the interval is only the longest time without a run.
If a unit still cycles after these changes, look at its learned setpoints for the current outdoor temperature in <json_path>/<app name>.json ("data", by outdoor temperature in 2-degree steps, then lux). Values far above the setpoint that holds the room on a calm day were learned while it cycled. Stop the app, remove those entries (or the whole "data" of that climate) and start it again; they are learned anew.
Each device can control a list of covers under screening. The covers need a current_position attribute (100 = open).
Closing. A device tries to close its screens in its own run when the room is warm, and also between runs: every time the room sensor in use reports a new value (see Room trigger).
The rules of the run:
| Device and situation | Tries to close when |
|---|---|
Indoor mode heater, or HVAC in heat, normal operation |
room more than 0.2 above the target, no wind at or above anemometer_speed in the last 45 minutes, outdoor temperature above screening_temp |
| Indoor mode heater, vacation | room above the target and outdoor temperature above screening_temp |
HVAC on vacation in heat |
when it switches to fan_only (see above) and outdoor temperature above screening_temp |
HVAC in cool |
every run |
HVAC in fan_only |
every run with outdoor temperature above screening_temp |
Table mode heater (needs indoor_sensor_temp and target_indoor_temp) |
same as an indoor mode heater, normal operation and vacation |
A close attempt closes a cover only when all of these are true:
- no rain (rain is 0) and wind below
anemometer_speed, - none of the screen's own
windowsensorsison, - nobody in
not_when_homeishome, - outdoor lux at or above
lux_close, - cloud cover below
cloud_cover(default 90 %), - the cover is fully open (position 100) and has not been moved by hand since the app last moved it.
If the attempt fails only because of lux or cloud cover, the screen waits: on the next WEATHER_CHANGE event with lux at or above lux_close and cloud cover below cloud_cover, it tries again. A run where the room is no longer warm stops the waiting (heaters too, since this version).
Closing used to wait for the next run, up to 30 minutes after the room got warm. Now the indoor sensor (indoor_sensor_temp, or backup_indoor_sensor_temp while the indoor sensor is unavailable or has not reported for an hour) triggers a close attempt as soon as it reports a value that satisfies the same rule as the run. Nothing to configure, it is always on.
- It uses the sensor's raw value. The daytime shifts of the run (
daytime_savings,daytime_increasing) and wind and rain corrections are not applied, and there is no hold time. - It only closes screens. It never changes the setpoint or the HVAC mode, never writes the learned data and never clears the waiting for lux (the run does that). With
controller: adaptivein indoor mode the same report also starts a run, see When a device runs. - At most one attempt per device every 5 minutes, however often the sensor reports.
- Heaters (indoor and table mode): the heater rules in the table above. HVAC: only in
heat, not on vacation;cool,fan_only,offand the vacation rule stay with the run. Withcontroller: adaptivethe controller's rule is used in every mode exceptcool, with the indoor sensor, or the backup sensor plus its learned bias; the estimate is never used. - Nothing happens without an indoor or backup sensor, without
target_indoor_temp, while a device window sensor ison, or right after a vacation ended (until the run that follows the end of the vacation, a few seconds later; that run starts the heating, see Cold start after vacation). - A room sensor that sits in the airflow of the unit swings and may cross the limit more often than the run noticed. The close attempt still needs the lux, cloud cover, wind, rain and window conditions below.
cloud_cover is a number (percent). The cloud cover comes from the Weather app's WEATHER_CHANGE event. If the Weather app does not send it, the value is 0 and this check always passes. Set it per app (one-device apps), per list entry, or once at the app level in the deprecated list style, where every device inherits it. It applies to all screens of the device; it cannot be set per screen.
Opening. On every WEATHER_CHANGE event, on every change of the outdoor lux, and when a deprecated rain_sensor reports rain, each screen opens when any of these is true:
- it rains (any rain above 0;
rain_levelis not used for screens), - wind is above
anemometer_speed, - outdoor lux is below
lux_open, or belowlux_open_mediawhile one of themediaplayershas the stateon.
A screen is not opened while one of its own windowsensors is on. A cover that was moved by hand after the app closed it is opened only when the outdoor lux is 400 or less.
ClimateCommander needs the ad-Weather app. It collects your weather sensors in one app and sends the WEATHER_CHANGE event with temp, rain, wind, lux and cloud_cover. Values it does not have are sent as None and ignored. The app name is weather unless you set weather_app. At startup ClimateCommander uses the last values the Weather app has sent, so it does not wait for the next change.
Outdoor values are kept per ClimateCommander app. Until the first outdoor temperature arrives, indoor mode uses 10 and table mode waits.
Deprecated own sensors: outside_temperature, rain_sensor and anemometer (read in HASS_namespace) still work and log a WARNING at startup. While such a sensor has sent a new value in the last 20 minutes, the same value from the Weather app is ignored. Configure them in the Weather app instead.
Own outdoor lux sensors (OutLux_sensor, OutLuxMQTT, OutLux_sensor_2, OutLuxMQTT_2), for screen control:
OutLux_sensorandOutLux_sensor_2are Home Assistant entities (read inHASS_namespace);OutLuxMQTTandOutLuxMQTT_2are MQTT topics (inMQTT_namespace). An MQTT payload can be a number, or JSON withilluminance(Zigbee) orvalue(Z-Wave).- With two sensors the highest of the values received in the last 15 minutes is used.
- When none of them has sent a new value for 20 minutes, or none is configured, the lux from the Weather app is used.
- The logic is in
weather_data.py, the same file as in Lightwand.
ElectricalManagement 1.0.5 or later can control a ClimateCommander device. List the app under climate_commander: in ElectricalManagement, together with the energy keys. Nothing is needed in this app. Example, in the ElectricalManagement configuration:
climate_commander:
- app: heater_bathroom
power: 2000
consumptionSensor: sensor.bathroom_floor_power
kWhconsumptionSensor: sensor.bathroom_floor_energy
pricedrop: 0.2
max_continuous_hours: 4app is the name of the ClimateCommander app, power the heater's rated power in watts. ElectricalManagement then sends one of these states:
| State | Table mode heater | Indoor mode (HVAC, heaters without a table) |
|---|---|---|
normal |
table setpoint | follows the indoor sensor |
save (high price) |
the save temperature | setpoint + save_temp_offset (default -0.3; cooling: +0.3) |
spend (low price, solar) |
table setpoint + spend_temp_offset (default 1) |
setpoint + spend_temp_offset (cooling: -1) |
reduce (over the kWh goal) |
lowest of the save and vacation temperature | same as save |
- A new state is applied at once.
- In indoor mode the setpoint is held at its value from before the state plus the offset, so the regulation does not work against the saving. If you change the setpoint by hand during a state, that becomes the new value to hold. Nothing is learned during a state. When the state ends, regulation continues from the held setpoint.
- In indoor mode, vacation and open windows win over the states.
- A state is valid for the time ElectricalManagement gives with it (45 minutes by default). Without a new call in that time the device returns to
normalby itself and logs an INFO message. - The keys of an ElectricalManagement
climate:entry work here too:heater(same asclimate),window_temp(same aswindow_sensor_temp),recipient(same asnotify_receiver) andoptions: [vacation_keep_off](same asvacation_keep_off: true), and the table keys (temperatures,target_heater_temp,save_temp, ...). The energy keys (consumptionSensor,pricedrop,max_continuous_hours, ...) stay in ElectricalManagement.
For developers: ElectricalManagement uses get_device(climate=None) on the app, and on the device set_power_state(state, valid_minutes, source), power_state, in_daytime_saving(), heater, namespace, vacation_entity and daytime_savings.
Types: string, number (integer or decimal), bool (true / false), list. "Entity" means an entity id string. Where a list is expected, a single value is accepted as a one-item list. Defaults marked C / F depend on the unit.
In a one-device app all keys (app-level and device keys) go at the top level of the app.
| Key | Type | Required | Default | Meaning |
|---|---|---|---|---|
module |
string | yes | Always climateCommander. |
|
class |
string | yes | Always Climate. |
|
HASS_namespace |
string | no | default |
Home Assistant namespace. Also the default for each device's namespace. |
MQTT_namespace |
string | no | mqtt |
MQTT namespace, used for OutLuxMQTT and OutLuxMQTT_2. |
weather_app |
string | no | weather |
Name of the ad-Weather app. An ERROR is logged if it is not found. |
unit_of_measurement |
string | no | from sensors | C or F. Without it the unit is read from outside_temperature, then from the devices' indoor_sensor_temp. If no unit is found the app stops. |
json_path |
string | no | none | Folder for <app name>.json (learned setpoints, HVAC fan mode). Must end with /. Without it nothing is learned. |
vacation |
entity | no | input_boolean.vacation if it exists |
input_boolean that turns on vacation. |
notify_app |
string | no | none | Name of your own notification app (it needs a send_notification method). Without it the Home Assistant notify service is used. |
notify_receiver |
list | no | none | Receivers of the window notifications, for example mobile_app_your_phone. One value or a list. Alias recipient. |
OutLux_sensor |
entity | no | none | Own outdoor lux sensor. |
OutLuxMQTT |
string | no | none | MQTT topic of an own outdoor lux sensor. |
OutLux_sensor_2 |
entity | no | none | Second own outdoor lux sensor. |
OutLuxMQTT_2 |
string | no | none | MQTT topic of a second own outdoor lux sensor. |
outside_temperature |
entity | no | none | Deprecated, configure it in ad-Weather. Outdoor temperature sensor. |
rain_sensor |
entity | no | none | Deprecated, configure it in ad-Weather. Rain sensor. |
anemometer |
entity | no | none | Deprecated, configure it in ad-Weather. Wind speed sensor. |
away_state |
entity | no | none | Deprecated, use vacation. Logs a WARNING. |
HVAC |
list | no | empty | Deprecated list style: device entries of type hvac. |
Heaters |
list | no | empty | Deprecated list style: device entries of type heater. |
In a one-device app these go at the top level. In the deprecated list style they go into each entry of HVAC: / Heaters:; keys marked "inherit" can also be set once at the app level.
| Key | Type | Required | Default | Meaning |
|---|---|---|---|---|
climate |
entity | yes | The climate entity to control. heater works as an alias. |
|
type |
string | no | heater |
heater or hvac. Ignored in the list style (the list decides). |
namespace |
string | no | HASS_namespace |
Namespace of all this device's entities. |
target_mode |
string | no | from the keys | indoor or table, see How the setpoint is found. |
indoor_sensor_temp |
entity | indoor mode | none | Indoor temperature sensor. |
backup_indoor_sensor_temp |
entity | no | none | Used when the indoor sensor is unavailable or has not reported for an hour (room_sensor_stale_minutes). |
target_indoor_temp |
number | no | 22.7 C / 72.8 F | Indoor target. In table mode: the room temperature above which the setpoint is lowered. |
target_indoor_input |
entity | no | none | input_number for the indoor target. Overrides target_indoor_temp. |
temperatures |
list | no | empty | Table mode: the table entries, see Table entry keys. |
target_heater_temp |
number | no | target_indoor_temp |
Table mode: heater setpoint at offset 0. |
target_heater_input |
entity | no | target_indoor_input if neither heater key is set |
Table mode: input_number for the heater setpoint. |
save_temp_offset |
number | no | -0.3 in indoor mode, none in table mode | Indoor mode: added to the held setpoint in save and reduce. Table mode: rule 1 of the save temperature. |
save_temp |
number | no | none | Table mode: save temperature (the entry's offset is added). Indoor mode heater: an open window sets the lowest of vacation_temp and save_temp. |
spend_temp_offset |
number | no | 1 | Added in spend (subtracted when cooling). Inherit. |
vacation |
entity | no | app-level vacation |
Own vacation input_boolean for this device. |
vacation_temp |
number | no | indoor mode 16 C / 61 F; table mode none | Vacation setpoint. In table mode the entry's offset is added. Inherit. |
max_vacation_temp |
number | no | 30 C / 86 F | HVAC on vacation: cool above this. Inherit. |
vacation_keep_off |
bool | no | false |
Table mode: turn the heater off on vacation. |
options |
list | no | empty | Flags. vacation_keep_off is the same as vacation_keep_off: true (ElectricalManagement style). has_fireplace (HVAC): heat to fan_only at once at target + 0.6, without lowering the setpoint first, see Heat pump cycling. Unknown names are ignored. |
interval_minutes |
number | no | 30 indoor, 15 table | Minutes between the periodic runs, 1 to 120. With controller: adaptive in indoor mode: the longest time without a run. |
min_run_gap_minutes |
number | no | 5 | Runs started by a sensor report or a person: at most one per this many minutes, 1 to 30. A report inside that time gets one run at its end. See When a device runs. |
room_sensor_stale_minutes |
number | no | 60 | After this long without a report indoor_sensor_temp counts as unavailable, 5 to 1440. See Sensors that report rarely. |
backup_sensor_stale_minutes |
number | no | 60 with adaptive and shadow, no limit with classic |
The same for backup_indoor_sensor_temp, 5 to 1440. |
window_sensor_stale_minutes |
number | no | 720 with adaptive and shadow, no limit with classic |
The same for window_sensor_temp, 5 to 4320. |
increase_hold_hours |
number | no | 2 | No heat to fan_only for this many hours after a daytime_increasing period stops applying (at its stop, or when its mode or presence ends it or daytime_savings replace it; none for an HVAC in fan_only then), 0 to 12 (0: no hold). See After an increase. |
mode_app |
string | no | none | Name of the ModeManagement app. Only used by periods with a mode: its current_mode is read, so the house mode is known after a restart. See House modes in periods. |
trend_hold_hours |
number | no | 2 | Indoor mode (classic): the setpoint is kept while the room reaches the target within this many hours at its trend, 0 to 12 (0: never kept). See the step rules. |
max_setpoint_drop |
number | no | 3 C / 5.4 F | HVAC in heat, indoor mode: lowest setpoint is target_indoor_temp minus this (at least the climate's min_temp). Above 0, at most 15. Also used by controller: adaptive. |
cold_start_deficit |
number | no | 2 C / 3.6 F | Indoor mode: when a vacation ends with the room at least this far below the target, the heating starts with the target plus the difference, see Cold start after vacation. 0.5 to 10. Also used by controller: adaptive. |
window_sensor_temp |
entity | no | none | Temperature sensor in a window (sun). window_temp works as an alias. |
window_offset |
number | no | -3 | The window sensor lowers the setpoint when it is above target_indoor_temp + window_offset. With controller: shadow / adaptive only the starting value of the learned window level (default -3 C / -5.4 F when not set). |
windowsensors |
list | no | empty | Window / door binary_sensors. on for 2 minutes counts as open. |
rain_level |
number | no | 3 | Rain at or above this raises the heating. Inherit. |
anemometer_speed |
number | no | 40 | Wind at or above this raises the heating and starts boost; screens open above it and do not close at or above it. Inherit. |
daytime_savings |
list | no | empty | Periods with a lower temperature, see Periods. |
daytime_increasing |
list | no | empty | Indoor mode: periods with a higher temperature. |
silence |
list | no | empty | HVAC: periods with fan mode Silence. |
automate |
entity | no | none | input_boolean. While it is off the device does nothing. |
screening |
list | no | empty | Covers, see Screening keys. |
screening_temp |
number | no | 8 C / 47 F | Screens close only when the outdoor temperature is above this (not used in HVAC cool). Inherit. |
cloud_cover |
number | no | 90 | Screens close only while the cloud cover (percent, from the Weather app) is below this. Inherit. |
getting_cold |
number | no | 18 C / 65 F | "Window open" notification when outdoor and indoor are below this; "Window closed" needs outdoor above it. Inherit. |
getting_hot_offset |
number | no | 10 | "Window closed" notification when the room is this much above the target. Inherit. |
notify_receiver |
list | no | app-level value | Receivers of this device's notifications. Alias recipient. Inherit. |
controller |
string | no | classic |
classic, shadow or adaptive, see Adaptive controller. |
diagnostic_sensor |
entity | no | none | Shadow / adaptive: sensor the app writes with the controller's terms, at most every 10 minutes while the setpoint and mode hold. |
window_offset_table |
list | no | none | Shadow / adaptive: starting values of the learned window level, see Keys. |
internal_offset_table |
list | no | none | Shadow / adaptive: starting values of the climate's sensor minus the room. |
target_table |
list | no | none | Shadow / adaptive: indoor target by the 24-hour mean outdoor temperature. |
outdoor_gain |
number | no | not set | Shadow / adaptive: fixed outdoor feed-forward, 0 to 0.6. |
solar_gain |
number | no | 1.0 | Shadow / adaptive: factor on the sun term, 0 to 5. |
solar_max |
number | no | 3 C / 5.4 F | Shadow / adaptive: largest sun term. |
comfort_guard |
number | no | 1.5 C / 2.7 F | Shadow / adaptive: power-state offset dropped when the room is this far off target. |
Each entry in screening:
| Key | Type | Required | Default | Meaning |
|---|---|---|---|---|
screen |
entity | yes | The cover entity. An entry without it is skipped with a WARNING. |
|
lux_close |
integer | no | 40000 | Close at or above this outdoor lux. |
lux_open |
integer | no | 15000 | Open below this outdoor lux. |
lux_open_media |
integer | no | 4000 | Open below this outdoor lux while a mediaplayers entity is on. |
windowsensors |
list | no | empty | Windows behind this screen. While one is on, the screen is neither closed nor opened. |
not_when_home |
list | no | empty | person entities. The screen does not close while one of them is home. |
mediaplayers |
list | no | empty | media_player or switch entities. Only the state on counts (not playing). |
The device keys screening_temp, cloud_cover and anemometer_speed also apply to the screens.
Each entry in temperatures:
| Key | Type | Required | Default | Meaning |
|---|---|---|---|---|
out |
number | yes | Outdoor temperature from which this entry applies (up to the next entry). | |
offset |
number | no | none | Added to target_heater_temp. Also added to save_temp and vacation_temp. |
normal |
number | no | none | Absolute setpoint, used when the entry has no offset. |
save |
number | no | none | Absolute save temperature, used when neither save_temp_offset nor save_temp is set. |
vacation |
number | no | none | Absolute vacation temperature, used when vacation_temp is not set. |
Other keys in an entry are accepted and ignored. Only out is checked by the validation.
Each entry in daytime_savings, daytime_increasing and silence:
| Key | Type | Required | Default | Meaning |
|---|---|---|---|---|
start |
string | yes (except daytime_savings with only presence, and entries with a mode) |
Start time, quoted, for example '10:00:00'. |
|
stop |
string | yes (same) | Stop time, quoted. | |
presence |
list | no | none | person entities, see Periods. |
mode |
string or list | no | none | daytime_savings / daytime_increasing only: the house mode(s) the entry applies in. With a mode, start and stop are optional. See House modes in periods. |
HVAC with screens, periods and notifications:
climate_living_room:
module: climateCommander
class: Climate
climate: climate.living_room
type: hvac
json_path: /conf/persistent/Climate/
vacation: input_boolean.vacation
automate: input_boolean.automate_climate
indoor_sensor_temp: sensor.living_room_temperature
backup_indoor_sensor_temp: sensor.living_room_heat_pump_temperature
target_indoor_temp: 22.7
window_sensor_temp: sensor.living_room_window_temperature
window_offset: -3
vacation_temp: 16
max_vacation_temp: 30
screening_temp: 8
cloud_cover: 80
getting_cold: 18
rain_level: 3
anemometer_speed: 40
notify_receiver:
- mobile_app_your_phone
windowsensors:
- binary_sensor.living_room_window
daytime_savings:
- start: '10:00:00'
stop: '14:00:00'
presence:
- person.resident_1
- person.resident_2
silence:
- start: '21:00:00'
stop: '07:00:00'
presence:
- person.resident_1
screening:
- screen: cover.living_room_screen
lux_close: 40000
lux_open: 15000
lux_open_media: 4000
not_when_home:
- person.resident_2
mediaplayers:
- switch.projector
- media_player.living_room_tv
windowsensors:
- binary_sensor.living_room_doorFloor heater in table mode with save, vacation and an input for the heater target:
heater_bathroom:
module: climateCommander
class: Climate
climate: climate.bathroom_floor
type: heater
vacation: input_boolean.vacation
indoor_sensor_temp: sensor.bathroom_temperature
target_indoor_temp: 24.2
target_heater_input: input_number.bathroom_floor_target
target_heater_temp: 28
save_temp_offset: -0.5
vacation_temp: 13
vacation_keep_off: false
windowsensors:
- binary_sensor.bathroom_window
daytime_savings:
- start: '09:00:00'
stop: '15:00:00'
temperatures:
- out: -6
offset: 3
- out: 3
offset: 1
- out: 7
offset: 0
- out: 15
offset: -3Deprecated list style with app-level keys inherited by every device:
climate_control:
module: climateCommander
class: Climate
json_path: /conf/persistent/Climate/
vacation: input_boolean.vacation
vacation_temp: 16
cloud_cover: 85
notify_receiver:
- mobile_app_your_phone
HVAC:
- climate: climate.living_room
indoor_sensor_temp: sensor.living_room_temperature
target_indoor_temp: 22.5
screening:
- screen: cover.living_room_screen
Heaters:
- climate: climate.office_panel_heater
indoor_sensor_temp: sensor.office_temperature
target_indoor_temp: 21
cloud_cover: 70The configuration is checked with pydantic models (AppConfig and DeviceConfig in cc_config.py) when the app starts.
- A value of the wrong type logs an ERROR naming the key, for example:
climate.bathroom_floor: invalid configuration, device skipped: window_offset: Input should be a valid number, unable to parse string as a number
A table entry withoutoutgivestemperatures.0.out: Field required. - An invalid device key skips that device. In a one-device app the app then runs without a device.
- An invalid app-level key (
HASS_namespace,MQTT_namespace,weather_app,json_path,vacation, the lux and deprecated sensor keys,HVAC,Heaters, ...) stops the app:Invalid configuration for <app>, app not started: .... - In the list style, an inherited app-level key (
vacation_temp,cloud_cover, ...) is checked per device, so a wrong value skips every device that inherits it. - A single value where a list is expected is accepted as a one-item list (
windowsensors,temperatures,daytime_savings,daytime_increasing,silence,screening,options,presence, and the screen keyswindowsensors,not_when_home,mediaplayers). This includesnotify_receiver. - Unknown keys are ignored without a message. Check the spelling of a key that seems to have no effect.
- Table entries: only
outis checked;offset,normal,saveandvacationare used as written. automatemust be an entity id;true/falseare accepted but ignored.- A
target_modeother thanindoorortableis ignored, and the mode is chosen from the keys. interval_minutesmust be 1 to 120,min_run_gap_minutes1 to 30,room_sensor_stale_minutesandbackup_sensor_stale_minutes5 to 1440,window_sensor_stale_minutes5 to 4320,window_trigger_delta0 to 20 ortrue/false,trend_hold_hours0 to 12,increase_hold_hours0 to 12,max_setpoint_dropabove 0 and at most 15,cold_start_deficit0.5 to 10 (in the unit of the device: in F at most 10 F). Flags go inoptions(options: [has_fireplace]);has_fireplace: trueas its own key is an unknown key and is ignored.- The adaptive controller keys are checked strictly:
controllermust beclassic,shadoworadaptive;outdoor_gain0 to 0.6,solar_gain0 to 5,solar_maxandcomfort_guardabove 0;diagnostic_sensormust look like an entity id; table entries may only have their documented keys, offsets between -30 and 30, noouttwice, andtarget_tableentries exactly one oftarget/offset.
- Window notifications go to
notify_receiver(orrecipient). - Without
notify_appthe Home Assistant servicenotify/<receiver>is called for each receiver, with a title and a message. Without receivers nothing is sent. - With
notify_appyour app'ssend_notification(message=..., message_title=..., message_recipient=..., also_if_not_home=False)is called. If the app is not found, a WARNING is logged and no notifications are sent. - With the
notify_mobilesapp from Pythm_AppdaemonApps setnotify_app: notify_mobiles(the name of yournotify_mobilesapp inapps.yaml). Thennotify_receiveris a list of the person labels you defined underrecipientsinnotify_mobiles(for example[Me]), not phone service names such asmobile_app_.... A label that does not exist there receives nothing. Withoutnotify_receiver, nobody is notified. A recipient that has atrackeronly receives while the tracker ishome, because ClimateCommander sends withalso_if_not_home=False. - "Window open": sent once after windows are opened, the first time both the outdoor and the indoor temperature are below
getting_cold. Message:Window near <name> is open and inside temperature is <temp>Β°. - "Window closed": sent once after all windows are closed, the first time the room is at or above the target plus
getting_hot_offset(default 10) and the outdoor temperature is abovegetting_cold. Message:No Window near <name> is open and it is getting hot inside! <temp>Β°. - The conditions are checked in each device run.
Messages in the AppDaemon log, and what they mean:
| Level | Message | Meaning and fix |
|---|---|---|
| ERROR | Invalid configuration for <app>, app not started: <key>: <problem> |
An app-level key has the wrong type. Fix the named key. |
| ERROR | <climate>: invalid configuration, device skipped: <key>: <problem> |
A device key has the wrong type. Fix the named key. |
| ERROR | Weather app 'weather' not found. ClimateCommander needs https://github.com/Pythm/ad-Weather ... |
Install ad-Weather, or set weather_app to its app name. Without it table mode never starts (unless the deprecated outside_temperature is set) and screens get no lux from it. |
| WARNING | Could not find unit of measurement. Please configure 'unit_of_measurement' with C or F. Aborting <app> setup |
No unit_of_measurement, and no indoor_sensor_temp (or outside_temperature) with a unit. Set unit_of_measurement. The app does nothing until fixed. |
| WARNING | Configuring several devices with 'HVAC:' / 'Heaters:' lists in one app is deprecated. ... |
Still works. Move to one app per device when convenient. |
| WARNING | 'outside_temperature' is deprecated. ... (also rain_sensor, anemometer) |
Still works. Configure the sensor in ad-Weather and remove it here. |
| WARNING | Please change 'away_state' to 'vacation' in your configuration. ... |
Rename the key. |
| WARNING | Unknown type '<type>' for <climate>. Use 'heater' or 'hvac'. Using 'heater'. |
Fix type. |
| WARNING | Skipping device without 'climate': {...} |
A list entry has neither climate nor heater. |
| WARNING | Notify app '<name>' not found. No notifications. |
Check notify_app against the name of your notification app. |
| WARNING | <climate>: window sensor <entity> has no last_changed. Does it exist? |
The window sensor id is wrong or the entity is missing. |
| WARNING | <climate>: screening entry without 'screen' is skipped: {...} |
Add screen: to the entry. |
| WARNING | <climate>: the entity does not exist in namespace '<ns>'. Nothing is controlled until it exists. Check 'climate'. |
The climate id is wrong or the integration is gone. Logged once, not every run. |
| INFO | <climate>: the entity is unavailable. Waiting for it to come back. / ... is back (<state>). |
Logged once per outage. |
| WARNING | Could not read <path>: <error>. Starting with empty data. |
The JSON file is damaged or unreadable. Learning starts again. |
| WARNING | Could not write <path>: <error> |
The json_path folder does not exist, is not writable, or the path does not end with /. The old file is left untouched. |
| WARNING | Unregistered AC state: <state>. <name>. Will not automate |
The HVAC unit is in a mode such as auto, dry or heat_cool. The app only handles heat, cool and fan_only. Logged in every run. |
| WARNING | <climate>: unknown power state '<state>' from <source>. |
ElectricalManagement sent a state other than normal, save, spend or reduce. Check the versions. |
| INFO | <climate>: indoor temp is None. Aborting setting new temperature |
HVAC without a usable indoor temperature. Check indoor_sensor_temp and the backup. |
| INFO | <climate>: power state '<state>' expired, back to normal. |
ElectricalManagement did not renew the state in time. |
| INFO | <input> is not a valid number. Using <value>. |
target_indoor_input has no number at startup; target_indoor_temp is used until it has. |
| INFO | Learned data for <climate> copied from <file>. |
First start of a one-device app: learned data was taken from the old app's JSON file. |
| DEBUG | <climate>: waiting for the outdoor temperature from the Weather app. |
Table mode has no outdoor temperature yet. |
| WARNING | <climate>: no fresh room sensor: using the climate's own sensor minus the learned offset (<x>). Integral frozen, no mode changes, no learning. |
Adaptive / shadow: the room sensor (and backup) is unavailable or silent for an hour. The room is estimated until it is back. Check the sensor's battery or connection. If the sensor is fine and only reports rarely, set room_sensor_stale_minutes, see Sensors that report rarely. |
| WARNING | <climate>: no fresh room sensor and no usable internal-sensor offset: open loop (feed-forward only). internal_offset_table gives an estimate. |
Adaptive / shadow: no room sensor and nothing learned yet about the climate's own sensor. Fix the sensor, or add internal_offset_table. |
| WARNING | <climate>: period time '<time>' not understood. The period starts and stops at the next run. |
The timer for a daytime_savings / daytime_increasing time could not be set. Check the time in the YAML. The period still works, but acts at the next run instead of 5 seconds after its time. |
| INFO | <climate>: room sensor back (primary). |
The estimate or open loop has ended. |
| INFO | <climate>: comfort guard: room <x>, target <y>, '<state>' offset dropped until the state changes. |
Adaptive: the room got comfort_guard too cold (save / reduce) or too warm (spend). Raise comfort_guard if this happens too often. |
| INFO | <climate>: setpoint changed by hand to <x>: followed. |
Adaptive: someone (or another app) changed the setpoint; the controller continues from it. |
| INFO | <climate>: adaptive state from another version or unit is ignored (learned models start empty). |
The "adaptive" entry in the JSON was saved in another unit or format. Learning starts again. |
| INFO | <climate>: <keys> only work with controller 'shadow' or 'adaptive'. Ignored with the classic controller. |
Remove the keys, or set controller. |
| INFO | <climate>: controller '<c>' is for indoor mode on HVAC ... the classic controller is used. |
An HVAC in table mode keeps the classic controller. |
| INFO | <climate>: controller '<c>' without json_path: ... |
Set json_path to keep what the controller learns over restarts. |
| WARNING | <climate>: window_offset_table needs window_sensor_temp. The table is not used. |
Add window_sensor_temp, or remove the table. |
| WARNING | <climate>: internal_offset_table is set, but the climate reports no current_temperature (yet). ... |
The climate has no own temperature (or it was not there at startup). The table only works when it reports one. |
| any | <climate>: [shadow] ... |
The same messages from a device in shadow: nothing was changed. |
| DEBUG | <climate>: shadow heat src primary R ... sp_ctrl ... send ... hold ... classic ... |
One line per run in shadow / adaptive with log_level: DEBUG, see Start with shadow. |
Nothing happens, and nothing is logged:
- The
automateentity isoff. - A heater in indoor mode has no indoor temperature, or the climate has no
temperatureattribute. - A table heater, or an HVAC unit, is
off. The app does not turn it on (except a table heater when vacation ends). - A cover has been moved by hand, or has no
current_position.
These are intended. Please do not report them as bugs.
- Indoor mode: small drops are rounded to -0.5 after 30 minutes, small rises wait 30 minutes (deliberate asymmetry).
- Indoor mode: the extra -1 when the setpoint has been 4 or more below the target for 4 hours.
- Cooling setpoints are not learned (they would mix with the heating data).
- Indoor mode: a learned setpoint is approached by at most 0.5 per run, not jumped to (or by the adjustment, if that is larger). All other setpoint moves are sent at once, see the step rules.
- Indoor mode: after a vacation the start setpoint can be up to 6 C above the target (the climate's
max_tempat most) until the room is within 1 C of the target. That is the cold start, not a fault. - HVAC without
has_fireplace: at target + 0.6 the setpoint is lowered first;fan_onlyonly at the lowest setpoint or while the room still rises (not seen on a sensor that reports less often than about every 25 minutes, see Sensors that report rarely). HVAC: nothing is learned within 2 hours of a mode change. - The "no cooling when it is cold outside" rule (
fan_onlyto cool only with the outdoor temperature abovetarget_indoor_temp - 4) applies with and withouthas_fireplace. - Learned setpoints are only updated in normal operation (not on vacation, with open windows, during daytime savings / increasing, or in an ElectricalManagement state).
- Table mode: open windows and
reduceuse the lowest of the save and vacation temperature. - Classic, shadow and table mode do not run on sensor reports; only
adaptivein indoor mode does, see When a device runs. - Table mode: an entry applies from its
outvalue and up. - In cooling, rain and wind make the app cool less (same direction as in heating).
- With two own lux sensors the highest fresh value is used. In the Weather app the lowest temperature and the highest lux are used.
input_boolean.vacationis the default vacation switch only if it exists (a convention shared with ElectricalManagement and ModeManagement).- Fixed values in the rules (steps of 0.3 / 0.5 / 1, the fallback 10,
getting_hot_offset10, 400 lux, the target + 4 of the fast way back) are not converted for Fahrenheit; only the defaults in the tables above are, and the 0.2 C per hour rise limit beforefan_only(0.36 F). - In table mode without an indoor sensor, the climate's own
current_temperatureis compared withtarget_indoor_temp. For a floor heater this can be the floor temperature: setindoor_sensor_temp, or atarget_indoor_tempthat suits that sensor. (controller: adaptiveleaves that comparison out, see Adaptive controller.) - The rules above (the asymmetry, the extra -1, the learned setpoints) belong to the classic controller. The adaptive controller's own deliberate choices are listed in What differs from classic.
- Found a bug? Open an issue or submit a PR!
- Want to add a feature? Discuss in the GitHub Discussions.
