Motion-Activated Lights
Turns a light on when a motion sensor detects movement, then turns it off automatically after a configurable delay once motion clears.
The Code
from hassette import App, AppConfig, D, states
from hassette.scheduler import ScheduledJob
MOTION_SENSOR = "binary_sensor.hallway_motion"
LIGHT = "light.hallway"
OFF_DELAY = 300 # seconds (5 minutes)
OFF_JOB_NAME = "motion_lights_off"
class MotionLightsConfig(AppConfig):
motion_sensor: str = MOTION_SENSOR
light: str = LIGHT
off_delay: float = OFF_DELAY
class MotionLights(App[MotionLightsConfig]):
_off_job: ScheduledJob | None = None
async def on_initialize(self) -> None:
await self.bus.on_state_change(
self.app_config.motion_sensor,
handler=self.on_motion,
name="motion_sensor",
)
async def on_motion(self, new_state: D.StateNew[states.BinarySensorState]) -> None:
if new_state.value is True:
# Motion detected — cancel any pending off job and turn the light on.
if self._off_job is not None:
self._off_job.cancel()
self._off_job = None
await self.api.turn_on(self.app_config.light, domain="light")
elif new_state.value is False:
# Motion cleared — schedule the light to turn off after the delay.
self._off_job = await self.scheduler.run_in(
self.turn_off_light,
delay=self.app_config.off_delay,
name=OFF_JOB_NAME,
)
async def turn_off_light(self) -> None:
self._off_job = None
await self.api.turn_off(self.app_config.light, domain="light")
How It Works
on_state_changesubscribes to every state transition on the motion sensor. The handler uses dependency injection (D.StateNew[states.BinarySensorState]) to receive the new state as a typed object — both"on"and"off"are handled in one place.- When state is
"on", any pending off job is cancelled before turning the light on — this resets the timeout if motion is detected again while the timer is running. - When state is
"off",run_inschedulesturn_off_lightto fire 5 minutes later. The job is stored onself._off_jobso it can be cancelled on re-trigger. - Named job (
OFF_JOB_NAME) keeps logs readable. Only one off job per app instance can exist with a given name — if you need multiple sensors driving the same light, give each instance a different name via config. - Config fields (
motion_sensor,light,off_delay) let you run the same app class for multiple rooms with different values inhassette.toml.
Variations
Shorter or longer timeout — Change off_delay in hassette.toml without touching the code:
[apps.hallway_motion]
module = "motion_lights"
class = "MotionLights"
off_delay = 60 # 1 minute
Multiple sensors, one light — Deploy the app twice under different names, each pointing to its own sensor. The shared light entity is fine; whichever sensor detects motion last wins.