Getting a room ready for a film is a handful of small steps that are easy to half-do: lights down, shades across, sound on. This recipe folds them into one. When the TV starts playing the lights drop to a low film level, and when you switch it off they come back up, so the room sets itself and clears itself around what you are watching.
Pausing does not end it. The scene lifts only when the TV is switched off or goes idle, so stepping out to refill a glass leaves the room as it was rather than throwing the lights back on mid-film. The film level is yours to set.
Bringing the lights back is a snapshot, not a fixed level. The recipe records what every light was doing the moment the film started and puts that back at the end, so a lamp that was off stays off and one that was at a quarter returns to a quarter. Starting a film in an already dark room does not leave you with the lights up afterwards.
Shades, sound, and a button
Add shades as the optional cover role and they close when the film starts and open when it ends, which is what makes daytime viewing bearable. Add a soundbar or receiver as the optional media-player role and it powers on with the film and off after. Add a wall button or remote as the optional scene-controller role and a press starts movie mode by hand, for anything the TV state does not report cleanly. Extra dimmable accent lights can join the scene too.
Files Selora AI's pipeline reads to install this recipe. The manifest declares roles, inputs, and integrations; the package files are Jinja-templated YAML applied to Home Assistant.
# Movie Mode — dim the lights when the TV starts playing and restore them when
# it stops, with optional shades, sound, a scene button, and accent lights.
#
# The TV is a media_player; its move to playing activates the scene and a move
# to off, standby, or idle deactivates it (a pause does not, so a mid-film break
# keeps the room set). The optional scene-controller is an event entity whose
# press also activates. Lights and dimmers are set by brightness percent so
# non-dimmable lights still switch with the scene. Covers close and open; an
# optional soundbar powers on and off.
slug: movie-mode
version: 1.0.0
title: Movie Mode
tagline: The TV starting dims the room, and switching it off brings the lights back.
description: >-
Dims the lights when the TV starts playing and restores them when it is
switched off or goes idle, leaving the room set through a pause. Optionally
closes shades, powers a soundbar, takes a scene button, and brings in accent
lights.
author: Selora AI
released: "2026-08-28"
min_integration_version: "0.12.0"
tags: [media, lighting, scenes]
roles:
# TVs — the scene driver. media_player. min_count 1.
- id: tvs
title: TV
kind: media_player
min_count: 1
# One TV. Deactivate restores the room when the TV stops, and with several
# picked the first one to go idle would restore the lights and open the
# shades while another was still playing.
max_count: 1
selection: required
description: >-
The TV whose playing state drives the scene.
# Lights dimmed and restored — multi-select. kind light. min_count 1.
- id: lights
title: Room lights
kind: light
min_count: 1
selection: required
description: >-
The lights the scene dims and restores.
# Optional soundbar / AV receiver powered on and off with the scene.
- id: speakers
title: Soundbar or receiver
kind: media_player
min_count: 0
selection: required
description: >-
Optional. A soundbar or AV receiver powered on when the film starts and
off when it ends. Leave empty to skip.
# Optional shades closed and opened with the scene. kind cover. min_count 0.
- id: covers
title: Shades
kind: cover
min_count: 0
selection: required
description: >-
Optional. Shades or blinds that close when the film starts and open when
it ends. Leave empty to skip.
# Optional scene-controller button. event entity; a press activates the scene.
- id: scene_buttons
title: Scene button
kind: event
min_count: 0
selection: required
description: >-
Optional. A wall button or remote whose press starts movie mode by hand.
Leave empty to drive it from the TV alone.
# Optional dimmable accent lights joined into the scene. kind light.
- id: dimmers
title: Accent lights
kind: light
min_count: 0
selection: required
description: >-
Optional. Extra dimmable accent lights dropped to the film level with the
rest. Leave empty to skip.
inputs:
# Brightness the lights drop to when the film starts.
- id: dim_brightness
type: number
label: Film brightness (percent)
description: >-
Brightness the lights drop to when the film starts.
default: 15
min: 1
max: 100
# There is deliberately no "restore brightness" input. The lights are put back
# from a scene.create snapshot taken when the film starts, so they return to
# exactly what they were, including staying off. A fixed level would switch on
# lights the homeowner had off.
# Whether the optional shades close and open with the scene.
- id: close_covers
type: boolean
label: Close shades with the film
description: >-
When on, the optional shades close as the film starts and open when it
ends.
default: true
package_files:
- package/helpers/scene_state.yaml.j2
- package/automations/movie_mode.yaml.j2
{# Movie Mode: one automation sets the scene, one lifts it.
Activate triggers on any TV moving to playing, and on a press of the optional
scene button (an event entity; not_from excludes the unavailable-to-value
change on restart so the scene does not fire on boot). It snapshots the
lights, dims the ones that are on, closes the shades, and powers the
soundbar on.
The snapshot is what makes lifting the scene a real restore. Setting a fixed
"restore" brightness instead would switch on every light the homeowner had
off, because light.turn_on with a brightness turns a light on: start a film
in a dark room and every lamp comes up at the end whether or not it was ever
lit. scene.create records each light's exact prior state and scene.turn_on
puts it back. The dim step is guarded per light for the same reason, so
movie mode never lights a lamp that was off.
Activate is gated on the active flag being off. Leaving the room set through
a pause means the resume is a fresh move into "playing", which fires activate
again; re-snapshotting there would capture the dimmed lights and lose the
room the homeowner started with.
Deactivate triggers on any TV moving to off, standby, or idle, but not on a
pause, so a mid-film break leaves the room set. The scene button triggers it
too, so a session started by hand can always be ended by hand. The whole lift is gated on
the active flag, which is only ever raised by activate. That way a TV
switched off without having played does not restore a snapshot that was
never taken, or throw the shades open at midnight. #}
{% set su = slug | replace('-', '_') %}
{% set all_lights = roles.lights + roles.dimmers %}
{% set active = 'input_boolean.' ~ su ~ '_active' %}
automation:
- id: selora_recipe_{{ su }}_activate
alias: "Movie Mode: activate"
description: >-
When the TV starts playing, or the scene button is pressed, dim the lights
and set the room.
mode: single
trigger:
{% for tv in roles.tvs %}
- platform: state
entity_id: {{ tv }}
to: "playing"
{% endfor %}
{% for button in roles.scene_buttons %}
- platform: state
entity_id: {{ button }}
not_from:
- "unavailable"
- "unknown"
{% endfor %}
condition:
# not_from only filters the state strings it lists. An entity created
# straight into its trigger state reports from_state as null and slips
# past it, which happens on startup and on any integration reload. Require
# a real prior state so entity registration cannot stand in for an event.
- condition: template
value_template: "{% raw %}{{ trigger.from_state is not none }}{% endraw %}"
# Only set the room when it is not already set. A TV going from paused
# back to playing is a state change into "playing" and fires the trigger
# again, so without this guard scene.create would run a second time and
# record the already-dimmed lights as the "before" state. The film would
# then end by restoring the film level rather than the room the homeowner
# started with, and the original state would be gone for good. The same
# guard keeps a second press of the scene button from overwriting it.
- condition: state
entity_id: {{ active }}
state: "off"
action:
- service: scene.create
data:
scene_id: {{ su }}_before
snapshot_entities:
{% for light in all_lights %}
- {{ light }}
{% endfor %}
- repeat:
for_each:
{% for light in all_lights %}
- {{ light }}
{% endfor %}
sequence:
- if:
- condition: template
value_template: "{% raw %}{{ is_state(repeat.item, 'on') }}{% endraw %}"
then:
- service: light.turn_on
target:
entity_id: "{% raw %}{{ repeat.item }}{% endraw %}"
data:
brightness_pct: {{ inputs.dim_brightness }}
{% if roles.covers and inputs.close_covers %}
- service: cover.close_cover
target:
entity_id:
{% for cover in roles.covers %}
- {{ cover }}
{% endfor %}
{% endif %}
{% if roles.speakers %}
# continue_on_error because media_player.turn_on needs the TURN_ON
# feature and plenty of real speakers do not report it, Sonos among them.
# Powering the soundbar is a convenience; it must not be able to abort the
# run and leave the flag unset with the room already dimmed.
- service: media_player.turn_on
continue_on_error: true
target:
entity_id:
{% for speaker in roles.speakers %}
- {{ speaker }}
{% endfor %}
{% endif %}
- service: input_boolean.turn_on
target:
entity_id: {{ active }}
- id: selora_recipe_{{ su }}_deactivate
alias: "Movie Mode: deactivate"
description: >-
When the TV is switched off or goes idle, put the lights back exactly as
they were and open the room.
mode: single
trigger:
{% for tv in roles.tvs %}
- platform: state
entity_id: {{ tv }}
to:
- "off"
- "standby"
- "idle"
{% endfor %}
{% for button in roles.scene_buttons %}
# The button appears on both automations, which turns it into a toggle.
# Started by hand with a source the TV does not report as playing, there
# is no off, standby or idle edge coming, so without this the room would
# stay dimmed and the flag raised with no way back. The active flag splits
# the press: exactly one of the two can pass its condition, and activate
# only raises the flag at the end of its run, after this one has already
# been evaluated.
- platform: state
entity_id: {{ button }}
not_from:
- "unavailable"
- "unknown"
{% endfor %}
condition:
# not_from only filters the state strings it lists. An entity created
# straight into its trigger state reports from_state as null and slips
# past it, which happens on startup and on any integration reload. Require
# a real prior state so entity registration cannot stand in for an event.
- condition: template
value_template: "{% raw %}{{ trigger.from_state is not none }}{% endraw %}"
- condition: state
entity_id: {{ active }}
state: "on"
action:
# continue_on_error because a scene made by scene.create lives in memory
# only and is gone after a restart, while the flag below is restored. A
# film playing when Home Assistant restarts therefore reaches this step
# with the scene missing; without this the step raises, the sequence
# aborts, the flag is never cleared and movie mode is stuck on for good.
# Failing here just means the lights keep the film level, which the
# homeowner can undo, rather than the recipe wedging itself.
- service: scene.turn_on
continue_on_error: true
target:
entity_id: scene.{{ su }}_before
{% if roles.covers and inputs.close_covers %}
- service: cover.open_cover
target:
entity_id:
{% for cover in roles.covers %}
- {{ cover }}
{% endfor %}
{% endif %}
{% if roles.speakers %}
# Same reasoning as turn_on on the activate side: a speaker without the
# feature must not strand the flag on and wedge movie mode.
- service: media_player.turn_off
continue_on_error: true
target:
entity_id:
{% for speaker in roles.speakers %}
- {{ speaker }}
{% endfor %}
{% endif %}
- service: input_boolean.turn_off
target:
entity_id: {{ active }}
{# Movie Mode helper: a flag marking that the recipe set the room, so lifting
the scene only ever undoes a scene it set. Activate raises it after taking
the snapshot; deactivate requires it, restores, and clears it. It survives a
restart, so a film playing when Home Assistant restarts still lifts cleanly
when the TV goes off. #}
input_boolean:
{{ slug | replace('-', '_') }}_active:
name: "Movie Mode: room is set"
icon: mdi:movie-open