Motion light
Turns the hall light on when someone walks by after sunset. Late at night it is dimmed, so it does not wake anyone. A few minutes after the motion stops, it turns off again.
You need a motion sensor and a dimmable light.
<?php
declare(strict_types=1);
namespace App;
use Stewart\Contracts\App;use Stewart\Contracts\Automation;use Stewart\Contracts\HaContext;use Stewart\Contracts\Service\ServiceTarget;use Stewart\Contracts\Sun\SunCalendar;use Stewart\Contracts\Time\Clock;use Stewart\Contracts\Time\Duration;
#[Automation(id: 'night-motion-light')]final readonly class NightMotionLight implements App{ public function __construct( private HaContext $ha, private SunCalendar $sun, private Clock $clock, private string $motionSensor = 'binary_sensor.hall_motion', private string $light = 'light.hall', private int $offAfterMinutes = 3, private int $dayBrightness = 100, private int $nightBrightness = 10, ) {}
public function initialize(): void { $this->ha->watchStateChanges($this->motionSensor) ->whenChangedTo('on') ->filter(fn(): bool => !$this->sun->isSunUp()) ->subscribe(function (): void { $brightness = $this->clock->isWithin('23:00', '06:00') ? $this->nightBrightness : $this->dayBrightness; $this->ha->callService('light', 'turn_on', ['brightness_pct' => $brightness], ServiceTarget::forEntities($this->light)); });
$this->ha->watchStateChanges($this->motionSensor) ->whenChangedTo('off', for: Duration::minutes($this->offAfterMinutes)) ->subscribe(function (): void { if ($this->ha->getState($this->light)?->state === 'on') { $this->ha->callService('light', 'turn_off', target: ServiceTarget::forEntities($this->light)); } }); }
public function dispose(): void {}}apps: night-motion-light: options: motionSensor: binary_sensor.hall_motion light: light.hall offAfterMinutes: 3 nightBrightness: 10How it works
Section titled “How it works”filter(fn () => !$this->sun->isSunUp())lets motion through only while the sun is down.Clock::isWithin('23:00', '06:00')picks the brightness. The window may cross midnight.whenChangedTo('off', for: …)turns the light off only once the sensor has reported no motion for the whole time. New motion starts the wait again.- The light is only turned off if it is still on, so a light someone already switched off is left alone.
<?php
declare(strict_types=1);
namespace App\Tests;
use App\NightMotionLight;use DateTimeImmutable;use PHPUnit\Framework\TestCase;use Stewart\Contracts\Time\Duration;use Stewart\Testing\App\AppHarness;
final class NightMotionLightTest extends TestCase{ public function testLateAtNightTheLightIsDim(): void { $harness = $this->startAt('2026-01-01 23:30 UTC');
$harness->ha->pushState('binary_sensor.hall_motion', 'on');
self::assertSame(['brightness_pct' => 10], $harness->ha->getLastCall()->data); }
public function testInTheEveningTheLightIsBright(): void { $harness = $this->startAt('2026-01-01 19:00 UTC');
$harness->ha->pushState('binary_sensor.hall_motion', 'on');
self::assertSame(['brightness_pct' => 100], $harness->ha->getLastCall()->data); }
public function testDuringTheDayNothingHappens(): void { $harness = $this->startAt('2026-01-01 12:00 UTC');
$harness->ha->pushState('binary_sensor.hall_motion', 'on');
self::assertTrue($harness->ha->calls->isEmpty()); }
public function testTheLightTurnsOffAfterTheMotionStops(): void { $harness = $this->startAt('2026-01-01 19:00 UTC'); $harness->ha->pushState('binary_sensor.hall_motion', 'on'); $harness->ha->pushState('light.hall', 'on'); $harness->ha->pushState('binary_sensor.hall_motion', 'off');
$harness->advanceTime(Duration::minutes(3));
self::assertSame('light.turn_off', $harness->ha->getLastCall()->getServiceName()); }
private function startAt(string $moment): AppHarness { $harness = new AppHarness(new DateTimeImmutable($moment)); $harness->ha->seedState('binary_sensor.hall_motion', 'off'); $harness->ha->seedState('light.hall', 'off'); $harness->startApp(NightMotionLight::class);
return $harness; }}Make it yours
Section titled “Make it yours”- Skip the dimming when someone is watching TV: add a check of
media_player.living_roomin the handler. - Leave a light alone after someone switched it by hand: see Your own changes.
- Several rooms: an app runs once, so copy the class under another name and ID, or watch an area filter and turn on the lights of the area where the motion was.