Triggers
Some moments are neither a state change nor an event: a person entering a zone, a template turning true, a calendar
event starting. Home Assistant detects these with triggers, and watchTrigger() lets an app use them. This app
turns the hall light on when Anna comes home and the ground floor off when she leaves:
<?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\Trigger\Collection\HaTriggerCollection;use Stewart\Contracts\Trigger\HaTrigger;use Stewart\Contracts\Trigger\TriggerEvent;use Stewart\Contracts\Trigger\ZoneTransition;
#[Automation(id: 'welcome-home')]final readonly class WelcomeHome implements App{ public function __construct( private HaContext $ha, private string $person = 'person.anna', ) {}
public function initialize(): void { $this->ha->watchTrigger($this->createTriggers())->subscribe(function (TriggerEvent $event): void { match ($event->getTriggerId()) { 'arrived' => $this->ha->callService('light', 'turn_on', target: ServiceTarget::forEntities('light.hall')), 'left' => $this->ha->callService('light', 'turn_off', target: ServiceTarget::forFloors('ground')), default => null, }; }); }
public function dispose(): void {}
private function createTriggers(): HaTriggerCollection { return HaTriggerCollection::fromList([ HaTrigger::onZoneTransition($this->person, 'zone.home', ZoneTransition::Enter, id: 'arrived'), HaTrigger::onZoneTransition($this->person, 'zone.home', ZoneTransition::Leave, id: 'left'), ]); }}watchTrigger() returns a stream of TriggerEvents, with the usual operators (filter, debounce, take, …).
Built-in triggers
Section titled “Built-in triggers”| Builder | Fires |
|---|---|
HaTrigger::onZoneTransition('person.anna', 'zone.home', ZoneTransition::Enter) |
When a person or device tracker enters or leaves a zone |
HaTrigger::whenTemplateTrue('{{ … }}', for: Duration::minutes(2)) |
When a template turns true, optionally after it stayed true that long |
HaTrigger::onTimePattern(minutes: '/5') |
On a clock pattern; hours, minutes and seconds each take '/n', '*' or a number |
HaTrigger::atTime('07:30') |
Every day at that time |
HaTrigger::atTime('input_datetime.wake_up') |
At the time an input_datetime or timestamp sensor holds |
HaTrigger::onSunrise() / onSunset(SunOffset::before(…)) |
At sunrise or sunset |
Each builder takes an optional id:, which comes back as $event->getTriggerId().
Prefer Stewart’s own tools where they exist. For times and the sun, a schedule keeps working
while Home Assistant is unreachable; a trigger does not. For state changes, watchStateChanges() runs inside Stewart
and needs no subscription in Home Assistant.
Any other trigger
Section titled “Any other trigger”Every other trigger Home Assistant has (calendar, numeric_state, conversation, device, …) goes in as the array
you would write in YAML:
$this->ha->watchTrigger(['trigger' => 'calendar', 'event' => 'start', 'entity_id' => 'calendar.bins']);Several triggers
Section titled “Several triggers”A HaTriggerCollection, as in the example, fires when any of its triggers fires. It is one subscription in Home
Assistant; the id: tells the triggers apart. A plain list of arrays works the same way.
Variables
Section titled “Variables”Template triggers can read values you pass:
$this->ha->watchTrigger( HaTrigger::whenTemplateTrue('{{ states(sensor) | float(0) > limit }}'), variables: ['sensor' => 'sensor.hall_temperature', 'limit' => 25],);What a TriggerEvent holds
Section titled “What a TriggerEvent holds”| Method | Returns |
|---|---|
getTriggerId() |
Your id:, or the trigger’s position in the list |
getPlatform() |
zone, template, time, … |
getValue('to_state'), getTriggerData() |
Home Assistant’s trigger variables |
$event->context |
Who caused it |
$event->firedAt |
When Stewart received it |
How triggers behave
Section titled “How triggers behave”- Shared. Apps that watch the same trigger with the same variables share one subscription in Home Assistant.
- Outages. After Stewart reconnects, every trigger is subscribed again. A trigger that would have fired while the connection was down is lost.
- Rejected triggers. Home Assistant checks a trigger when Stewart subscribes. If it refuses, Stewart logs Home
Assistant’s reason and the subscription ends. The builders catch most mistakes earlier, with a
TriggerException. - Home Assistant version. The builders need Home Assistant 2024.10 or newer. On older versions, pass arrays with
platform:instead oftrigger:.
Testing
Section titled “Testing”The harness records what an app watches. pushTrigger() fires every watcher of an equal trigger:
<?php
declare(strict_types=1);
namespace App\Tests;
use App\WelcomeHome;use PHPUnit\Framework\TestCase;use Stewart\Contracts\Trigger\Collection\HaTriggerCollection;use Stewart\Contracts\Trigger\HaTrigger;use Stewart\Contracts\Trigger\TriggerEvent;use Stewart\Contracts\Trigger\ZoneTransition;use Stewart\Testing\App\AppHarness;
final class WelcomeHomeTest extends TestCase{ public function testArrivingTurnsTheHallLightOn(): void { $harness = new AppHarness(); $harness->startApp(WelcomeHome::class);
$watched = HaTriggerCollection::fromList([ HaTrigger::onZoneTransition('person.anna', 'zone.home', ZoneTransition::Enter, id: 'arrived'), HaTrigger::onZoneTransition('person.anna', 'zone.home', ZoneTransition::Leave, id: 'left'), ]); $harness->ha->pushTrigger($watched, new TriggerEvent(['platform' => 'zone', 'id' => 'arrived']));
self::assertSame('light.turn_on', $harness->ha->getLastCall()->getServiceName()); self::assertCount(1, $harness->ha->listWatchedTriggers()); }}Common problems
Section titled “Common problems”The trigger never fires, and the log shows Home Assistant’s error. Home Assistant rejected the trigger. Try the same trigger in a Home Assistant automation to see the problem.
It missed a moment. The connection was down at that moment; triggers are not replayed. Use a schedule for times of day.