Skip to content

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.

apps/NightMotionLight.php
<?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: 10
  • 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.
tests/NightMotionLightTest.php
<?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;
}
}
  • Skip the dimming when someone is watching TV: add a check of media_player.living_room in 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.