Automatic day-mode and thermostat-mode management for Home Assistant, driven by your calendar.
- HomeShift β Home Assistant Custom Integration
- Open HACS in Home Assistant
- Go to Integrations
- Click the three-dot menu β Custom repositories
- Add
https://github.com/Gamso/homeshiftwith category Integration - Search for HomeShift and install it
- Restart Home Assistant
HomeShift is a custom Home Assistant integration that automatically manages day modes (e.g. Home, Work, Remote, Away) and thermostat modes (e.g. Heating, Cooling, Off) based on your calendar events, weekends, and public holidays.
At regular intervals (every 5 minutes by default), it reads your calendar, picks the right day mode, and turns the matching scheduler switches on or off β so your home adapts automatically without any manual intervention.
- Reads the active event from your work/schedule calendar
- Optionally checks a public holiday calendar
- Determines the day mode based on a configurable event β mode mapping
- Turns on the scheduler switches for the active mode, and turns off all others
- Home Assistant 2026.9 or newer
- A calendar entity containing your work or schedule events
- A calendar entity for public holidays
- The Scheduler integration to automate scheduler switches
Scheduler tags (required for thermostat integration): When using the Scheduler integration alongside
thermostat_mode, each scheduler switch that controls heating or cooling must have the matching thermostat tag (e.g.Heating,Cooling). Schedulers without any thermostat tag are treated as day-mode-only and are never force-disabled by the thermostat logic. See the Thermostat Tags section for details.
- Go to Settings β Devices & Services β Add Integration β HomeShift
- Select your work calendar entity
- Optionally select a holiday calendar
- Configure your day modes and thermostat modes, or keep the defaults
- Save β HomeShift starts working immediately
Once set up, HomeShift will:
- Periodically read your calendar (every 5 minutes by default)
- Automatically update
select.day_mode - Turn the right scheduler switches on and off
Shows and controls the current day mode. HomeShift updates it automatically based on your calendar, but you can also change it manually at any time.
- Type: Select
- Default options:
Home,Work,Remote,Away - Writable: Yes β a manual change can be protected from auto-updates using the override duration
Shows and controls the current thermostat mode.
- Type: Select
- Default options:
Off,Heating,Cooling,Ventilation - Writable: Yes
When you manually change the day mode, this setting defines how long (in minutes) HomeShift waits before resuming automatic updates. Set to 0 to always allow automatic updates.
- Type: Number
- Default:
0(disabled)
Pre-activates an upcoming timed calendar event before it officially starts. When set, HomeShift switches to the correct day mode up to X minutes before the event start.
- Type: Number (minutes, 0β480, step 5)
- Default:
0(disabled) - Only applies to timed events β all-day events are always ignored.
Example: You have a Remote work event from 14:00 to 18:00 and early_switch = 120. HomeShift will switch to Remote working mode at 12:00, giving your heating schedule 2 hours to warm the house before you start working.
The sensor.next_mode_at and sensor.next_mode sensors reflect this anticipated switch time, so you can display it on a dashboard.
Shows the predicted next day mode that HomeShift will switch to.
- Type: Sensor (text)
- Value: Display name of the predicted next mode (e.g.
Remote working). When no change is expected in the next 2 days, shows the current day mode.
Shows when the next automatic mode change is expected to occur (taking early_switch into account for timed events).
- Type: Sensor (timestamp)
- Unit: ISO 8601 datetime
Shows the cover opening time computed for today by the Daily Cover Schedule feature.
- Type: Sensor (text)
- Value:
HH:MMstring (e.g.07:45), orunknownif not configured or has not run yet. - Only registered when at least one cover is listed under Individual Covers.
Shows the estimated daily cover closing time for today, computed by the Daily Cover Schedule feature from the configured sun elevation.
- Type: Sensor (text)
- Value:
HH:MMstring (e.g.21:40), orunknownif not configured or not yet computed. - Attributes:
sun_elevationβ the configured elevation the time was computed fromtriggerβelevationnormally,sunseton a day the sun never reached it and the fallback stepped in
- Only registered when at least one cover is listed under Individual Covers.
Warns that tonight's close had to leave covers up, and names them.
- Type: Binary sensor (
device_class: problem) - Value:
onwhen at least one cover was skipped by the evening close,offotherwise. - Attributes:
coversβ the entity ids left open, e.g.["cover.volet_chambre"]window_sensorsβ the sensor that blocked each one, e.g.{"cover.volet_chambre": "binary_sensor.fenetre_chambre"}count,checked_onβ how many, and the day the close ran
- Clears when the next calendar day's schedule is computed, not when the window is closed β the cover stays up either way until someone acts on it.
- Only registered when at least one cover is listed under Individual Covers.
- Survives a restart: the list is persisted with the rest of the day's cover state.
A notification automation can read the list straight out of the attributes:
automation:
- alias: Volets restΓ©s ouverts
triggers:
- trigger: state
entity_id: binary_sensor.covers_left_open
to: "on"
actions:
- action: notify.persistent_notification
data:
message: >
Volets non fermΓ©s ce soir :
{{ state_attr('binary_sensor.covers_left_open', 'covers') | join(', ') }}Immediately refreshes the scheduler switches based on the current day mode and thermostat mode. Useful after manually changing a mode.
Manually triggers a calendar check and updates select.day_mode if needed. This is also called automatically at regular intervals.
All parameters can be changed at any time via Settings β Devices & Services β HomeShift β Configure.
| Parameter | Default | Description |
|---|---|---|
| Work Calendar | β | Calendar entity containing your work/schedule events |
| Holiday Calendar | β | Calendar entity for public holidays (optional) |
| Day Modes | Home, Work, Remote, Away |
Comma-separated list of available day modes |
| Thermostat Mode Map | off:Off, heating:Heating, ... |
Maps internal thermostat keys to the display names you prefer |
| Scan Interval | 5 min |
How often HomeShift checks the calendar (in minutes) |
| Override Duration | 0 (disabled) |
Minutes to block automatic updates after a manual mode change |
| Early Switch | 0 (disabled) |
Minutes to pre-activate a timed event before its start |
| Default Mode | Work |
Mode used on regular weekdays with no calendar event |
| Weekend Mode | Home |
Mode used on Saturdays and Sundays |
| Holiday Mode | Home |
Mode used on public holidays |
| Event Mode Map | Vacation:home, Remote:remote |
Maps calendar event names to day modes |
| Away Mode | Away |
When you select this mode, automatic updates are paused until you leave it |
| Cover Entities | β | Cover entities to close when it is too hot (optional; requires Individual Covers below to be configured too) |
| Temperature Sensor | β | Sensor providing the outdoor temperature |
| Temperature Threshold | 30 Β°C |
Temperature above which covers close reactively (fallback for days the forecast misses) |
| Cover Action | close_cover |
Service called when heat protection triggers: close_cover or stop_cover |
| My Position Button | β | Button entity to press instead of a cover service (e.g. Somfy RTS "My" position) |
| Weather Entity | β | Weather entity with daily forecasts, used for the proactive close (optional) |
| Forecast Threshold | 28 Β°C |
Forecast daily high above which covers close proactively |
| Individual Covers | β | The covers opened and closed daily, added one at a time, each with an optional window sensor and an optional My position button (separate from Cover Entities above); Cover Heat Protection's active window is derived from this schedule |
| Open Time β (per day mode) | 08:30 |
One field per day mode: sunrise, skip, or a custom HH:MM value |
| Earliest Open Time | 07:00 |
Floor time used when a day mode's Open Time is sunrise |
| Sun Elevation At Closing | -2Β° |
Covers close when the descending sun reaches this many degrees above the horizon, every day, for every mode β the same scale as {{ state_attr('sun.sun', 'elevation') }}. 0Β° is sunset, negative is below the horizon |
Each time HomeShift refreshes, it looks at today's active calendar event and determines the day mode using this priority order:
| Priority | Condition | Resulting mode |
|---|---|---|
| 1 | Active calendar event matches the event mode map | Mapped mode (e.g. Remote) |
| 2 | A timed event starts within early_switch minutes and it matches the map |
Mapped mode (anticipated) |
| 3 | Today is Saturday or Sunday | Weekend mode |
| 4 | Today is a public holiday | Holiday mode |
| 5 | No special condition | Default mode (e.g. Work) |
Note: If you select the Away mode yourself, all automatic updates are paused until you change it manually β that is the point of the mode.
A calendar event mapped to the Away mode does not pause anything: it sets the mode like any other event, and the mode moves on normally once the event ends. Otherwise a single mapped event would freeze the integration for good.
If a calendar event covers only the morning or only the afternoon, HomeShift applies the corresponding mode only during that half of the day, then reverts to the default mode for the other half.
The number.early_switch entity lets you anticipate timed calendar events. When the current time is within the early-switch window before a timed event, HomeShift pre-activates the corresponding mode.
Calendar event: Remote working 14:00 ββββββββββββ 18:00
early_switch = 120 min
β
12:00 β HomeShift switches to Remote working here
Key rules:
- Only applies to timed events (events with a specific start/end time). All-day events (e.g. public holidays) are never pre-activated.
- The
sensor.next_modeandsensor.next_mode_atsensors reflect the anticipated switch time, not the original event start. - Setting
early_switchto0disables the feature entirely.
HomeShift can automatically turn scheduler switches on and off based on the current day mode.
In the integration settings, you can assign one or more switch entities to each day mode. When the day mode changes:
- The switches for the active mode are turned on
- The switches for all other modes are turned off
This lets you, for example, run different heating schedules depending on whether you're working from home or at the office β without any automation to write.
When you also use thermostat_mode, HomeShift needs to know which scheduler switches control heating or cooling so it can disable them automatically when the thermostat is off.
To make this work, each scheduler switch that is linked to a specific thermostat mode must have the corresponding thermostat mode name set as a tag in the Scheduler card.
Example:
Suppose your thermostat modes are Heating and Cooling. You create the following schedulers:
| Scheduler switch | Tags | Purpose |
|---|---|---|
switch.schedule_home_heating |
Heating |
Heating schedule when you're at home |
switch.schedule_work_heating |
Heating |
Heating schedule when you're at work |
switch.schedule_home_cooling |
Cooling |
Cooling schedule when you're at home |
switch.schedule_presence_light |
(no tag) | Lighting schedule, not thermostat-related |
When thermostat_mode is set to Off, HomeShift will force-disable all switches tagged with Heating or Cooling, regardless of the current day mode. Switches without any thermostat tag (like switch.schedule_presence_light) are left untouched.
How to add a tag in the Scheduler card: Open the Scheduler card β edit a schedule β scroll to Tags β add the thermostat mode name exactly as defined in your thermostat mode map (e.g.
Heating,Cooling).
HomeShift can natively open and close covers every day, without depending on any Scheduler-integration entity. The covers it drives are listed under Individual Covers β one entry per cover, a whole-house cover group counting as one β while this section sets the times. That list is separate from Cover Heat Protection's Cover Entities, so a south-facing cover can stay under heat-protection's control while the rest follow the daily open/close schedule below. Cover Heat Protection derives its active window from the times computed here, so configure both.
Once per day, shortly after midnight, HomeShift computes:
-
Open time β resolved per day mode. Each configured day mode has its own Open Time field, set to one of:
sunriseβ sunrise, floored at Earliest Open Time (e.g. never before07:00)skipβ no automatic opening for that mode (covers stay as they are)- a custom
HH:MMvalue β a fixed clock time
Day modes sharing the same value effectively form a batch (e.g. both
WorkandRemoteset tosunrise). A day mode with no value configured falls back to08:30. There's no separate "skip modes" list β set a mode's Open Time toskipdirectly. -
Close time β when the setting sun reaches the configured elevation, always, for every day mode (closing is not mode-dependent): see below.
A one-shot timer fires the open/close action at the exact scheduled minute; the periodic coordinator poll (5 minutes by default) acts as a fallback in case the timer is missed (e.g. a HA restart). Each action fires at most once per calendar day.
sensor.cover_open_time and sensor.cover_close_time reflect today's computed times, so you can display them on your dashboard.
The covers close when the descending sun reaches Sun Elevation At Closing degrees above the horizon β the value you would read from {{ state_attr('sun.sun', 'elevation') }}, on the same scale. You are setting a light level, not a delay:
| Elevation | Roughly | Light |
|---|---|---|
+2Β° |
Sunset β ~15 min | The sun is still up |
0Β° |
Sunset | The sun touches the horizon |
-2Β° (default) |
Sunset + ~10 min | Dusk, still easy to see outside |
-4Β° |
Sunset + ~20-25 min | Room is dark |
-6Β° |
Sunset + ~35-40 min | End of civil twilight, artificial light needed |
The "roughly" column is a mid-latitude approximation: the delay an elevation works out to shifts a little with latitude and season, which is the point β the light level does not.
Degrees say nothing about when your covers will actually move, so the Daily Cover Schedule form turns the value into tonight's time, computed from your own location:
At -2Β°, that is 21:41 tonight.
-2Β° is the default because it is where the retired "sunset + 10 minutes" setting used to land, at every season. Pick -4Β° to wait until the room is genuinely dark (about a quarter of an hour later), or a positive value to close before the sun is down.
The form refuses an elevation the sun never reaches at your location β a positive value that the midwinter sun stays below, or any value above the polar circle in June. At mid-latitude nothing in the range can be refused: the sun sweeps all of it every day of the year. Should it become unreachable anyway (a home that moved, an entry restored elsewhere), the covers close at plain sunset rather than staying up all night:
WARNING ... Daily cover schedule: the sun never reaches 8.0Β° on 2026-06-21 β closing at sunset instead
Upgrading from 1.7.x: the evening close used to be "sunset Β± N minutes". That setting is gone; the entry migration converts your offset into the elevation it was landing on, so the covers keep moving at the time they moved before. The conversion is logged.
The covers driven by the daily schedule are added one at a time from the Individual Covers menu: pick the cover, and optionally a window opening sensor (does that window stand open?) and a My position button (how should this cover close?). Adding a cover that is already in the list updates its settings instead of duplicating it. A cover group is a cover entity like any other, so driving the whole house through one group is simply a single entry.
The window sensor changes one thing: the evening close skips a cover whose window is reported open. Rather than closing a cover over an open window, HomeShift leaves it alone and logs a warning:
WARNING ... Daily cover schedule: not closing cover 'cover.volet_chambre' β window sensor 'binary_sensor.fenetre_chambre' reports the window open
Some covers shouldn't come all the way down in the evening. Give such a cover a My position button β the button entity that sends a Somfy RTS (or similar) cover to its recorded favourite position β and the evening close presses that button instead of sending close_cover to it:
INFO ... Daily cover schedule: pressing My position button 'button.my_salon' for cover 'cover.volet_salon' (close_time=21:40)
- The cover is held back from the bulk
close_coverβ it only ever receives its button press. - An open window still wins: a blocked cover gets neither a close nor a press.
- The morning open is unchanged β the My position only changes how a cover closes.
- This is per-cover, and independent of Cover Heat Protection's own My Position Button setting.
Details worth knowing:
- The morning open is never skipped β an open window is only a reason not to close.
- A skipped cover is not retried later that night. The day's close is marked done once it runs; the warning is the signal to close that cover by hand if you want it closed.
- A sensor that is missing or
unavailablecloses the cover as usual, with a warning β an unavailable sensor can't establish that the window is open. - A cover reached through a group is not protected. HomeShift sends the close to the entity ids you configured and does not look inside a group, so a cover that should keep its own window sensor or My position must be listed here individually (and dropped from the group).
- Covers without a window sensor behave exactly as before.
Migrating from Scheduler-integration volet entities: if you previously used two Scheduler entities (a fixed/sunrise-based "open" and a sunset-offset "close") purely to drive covers, you can disable/delete them once Daily Cover Schedule is configured with the same times β HomeShift no longer needs the Scheduler integration for covers at all.
HomeShift can automatically close a cover to prevent heat build-up β without requiring any separate automation, and without a separately configured time window. Once closed by this automation, the cover stays closed for the rest of the day; it never reopens itself. It's touched again either by the next day's normal open, or by Daily Cover Schedule's own unconditional evening close.
Cover Heat Protection now requires Daily Cover Schedule to be configured. Its active window isn't set independently β it's exactly [cover_open_time, daily_close_time], the same times Daily Cover Schedule already computes every day. This also means: if it's already hot right when the cover would normally open for the day, heat protection can apply the closed/protected position immediately instead of opening it and closing it again moments later.
Each time the coordinator runs and whenever the temperature sensor value changes, HomeShift checks: if the cover hasn't already been closed by this automation today, and the current time is within today's [cover_open_time, daily_close_time] window, and the outdoor temperature exceeds Temperature Threshold, it applies the configured Cover Action: close_cover (default), stop_cover (interrupts movement mid-travel β useful for Somfy RTS covers), or presses the My Position Button if one is configured (sends the cover to its pre-recorded favourite position).
This fires at most once per day β once closed, HomeShift leaves the cover alone regardless of what the temperature does afterward.
If a Weather Entity (with daily forecasts) is configured, HomeShift also checks β once per day, at today's cover_open_time β whether today's forecast high exceeds Forecast Threshold. If it does, the cover closes immediately (or skips opening in the first place, if this runs before/at the same moment Daily Cover Schedule opens it), ahead of the outdoor sensor actually crossing the reactive threshold.
The reactive close above still runs during the rest of the window as a fallback, in case the forecast lookup fails or under-predicts the day.
Leave Weather Entity unset to disable the proactive close entirely; behavior then falls back to the reactive close only.
Whether the cover has already been closed by this automation today, and when the forecast was last checked, are persisted to storage, so a Home Assistant restart mid-day doesn't lose track of the day's state. The same storage also tracks whether today's Daily Cover Schedule open/close actions have already run, for the same reason.
| Feature | Status | Notes |
|---|---|---|
| Calendar-driven day mode | β | See Detection Logic |
| Thermostat mode + scheduler tags | β | See Thermostat Tags |
| Half-day event support | β | See Half-Day Events |
| Early switch (pre-activation) | β | See Early Switch |
| Manual override with timeout | β | number.override_duration |
| Native daily cover open/close (no Scheduler entity needed) | β | See Daily Cover Schedule |
| Daily cover schedule state survives HA restart | β | Persisted alongside the heat-protection cover state |
| Evening close driven by the sun's elevation | β | See When the Covers Close |
| Skip the evening close when a window is open | β | See Individual Covers |
| Warning entity listing the covers left open | β | binary_sensor.covers_left_open |
| Per-cover My position instead of a full close | β | See Individual Covers |
| Cover reactive heat close | β | See Reactive Close; active window derived from Daily Cover Schedule; never reopens itself |
| Cover proactive forecast-based close | β | See Proactive Forecast-Based Close |
| Cover automation state survives HA restart | β | See State Persistence |
| Day-mode/thermostat-mode state survives HA restart | β | Restored from storage at startup |
| Cover position feedback (open/closed state) | β | Not tracked β commands are sent blind; HomeShift only tracks whether it closed a cover today, not the cover's actual position |
This project is licensed under the MIT License.