Repository navigation
Releases: stewart-php/stewart
Release list
v2.1.0
Stewart 2.1 keeps your exposed entities through a Home Assistant restart. Stewart no longer gives up when it
reconnects before Home Assistant has loaded the Stewart integration, and a daemon that exposes no entities can leave
the integration alone.
Highlights
Entities come back after a Home Assistant restart. Home Assistant accepts connections before it has loaded the
Stewart integration, so Stewart used to reconnect, find no integration, and leave every exposed entity unavailable
until you restarted Stewart. It now keeps checking with the same backoff it uses to reach Home Assistant, and opens
the session as soon as the integration answers. Installing the integration while Stewart runs works the same way,
without a restart.
Exposing while the integration loads. An app that calls expose*() before the integration answers, for example
when Stewart and Home Assistant start together, gets its handle as usual. Its entity ID is null until the
integration answers, and then Stewart creates the entity. Before, the call threw ComponentMissing and the entity
stayed away until the app exposed it again.
Turn the integration off. A daemon that never exposes entities can set expose.enabled: false
(STEWART_EXPOSE__ENABLED=false). Stewart then sends nothing to the integration, make status and make doctor say
it is off, and expose*() throws ExposureError::Disabled.
Also new
- The log line,
make statusand the exposure error for a missing integration say it may still be loading, not only
that it is not installed. ExposureError::ComponentMissingis deprecated: Stewart no longer throws it, and 3.0 removes it.
Upgrading
Run make update, then restart Stewart. Nothing in your apps needs to change. Update the Stewart integration in Home
Assistant to 2.1.0 too; its protocol is unchanged, so the 2.0.0 integration keeps working until you do.
Every change is in the changelog.
v2.0.0
Stewart 2.0 gives every part one word: Home Assistant's actions are actions everywhere, apps have one way to run code
later, and new projects get a namespace of their own. It also removes everything 1.x deprecated. One upgrading
command rewrites most of an existing project, and anything it leaves behind is named when Stewart starts, not found
when an app fails.
Highlights
Actions, from the call to the metrics. The types for calling an action carry its name, and so do the generated
classes:
$this->ha->callAction('light.turn_on', ['brightness_pct' => 60], ActionTarget::forAreas('hall'));
$this->actions->notify->mobileAppPhone(message: 'Motion in the hall');ServiceTarget, ServiceResponse and ServiceCallException are now ActionTarget, ActionResponse and
ActionCallException, and the generated Services is Actions. The limits and dry run that cover every call to Home
Assistant are ha_calls, and the metrics follow: stewart_app_ha_calls_total and the rest. "Service" now means
only your own classes in services.php.
One way to run code later. Apps no longer get Timers; runAfter() is the delay, shows in make status and
in traces, and Scheduler::waitFor() builds a wait of your own. How long an entity has held its state needs no
clock any more:
$quiet = $this->ha->getState('binary_sensor.hall_motion')?->isOff() === true
&& $this->ha->hasHeldStateFor('binary_sensor.hall_motion', Duration::minutes(10));A namespace for new projects. New projects put their apps under Automations\ instead of App\, which clashed
with the Stewart\Contracts\App interface every app imports. The generated classes take their namespace from
composer.json, so existing App\ projects need no setting.
Upgrades that explain themselves. Generated classes from 1.x, an old service_calls setting or a
STEWART_SERVICE_CALLS__… variable stop make check, the deploy gate and the start with one line that says what
to run. make stan reports old generated classes once instead of crashing on them.
Also new
make run DRY_RUN=1is nowstewart run --dry-run, so the Makefile no longer names a setting.- Metrics are served only on the metrics port; the admin port and the chart's
adminApi.podMonitorno longer offer
them. stewart generateno longer asks for--allow-shrinkwhen classes are only renamed.- Everything deprecated in 1.x is removed:
callService()and its test stubs,dispose()on apps without
Disposable, andActionManifest.
Upgrading
Run make upgrade VERSION=2.0, then the command from the
upgrading guide, then make generate, make stan and
make test. The command rewrites the renamed classes and settings in your apps, tests and configuration; make stan
points at the few hand edits left, such as an app that used Timers. Rename the metrics in dashboards and alerts,
and upgrade the Home Assistant integration and the Helm chart to 2.0.0 with the rest. The 1.x documentation stays at
stewart-php.github.io/docs/1.x, and runtime:1 images keep getting PHP and
Alpine fixes until the end of April 2027.
Every change is in the changelog.
v1.8.0
Stewart 1.8 means fewer copies and fewer typos: call actions by the names Home Assistant uses, run one app class for
every room, and let PhpStorm and PHPStan catch a mistyped entity ID or action name before Home Assistant sees it.
Highlights
Call actions by their names. callAction() takes the action as Home Assistant writes it, the same name you see
under Developer tools → Actions:
$this->ha->callAction('light.turn_on', ['brightness_pct' => 60], ServiceTarget::forEntities('light.hall'));
$this->ha->getEntity('lock.front_door')->callAction('lock');callService('light', 'turn_on', …) still works, but each call prints a deprecation line, and 2.0 removes it. The
upgrading guide has a one-line command that rewrites the old calls. The guide is now
Call actions.
One class, several apps. Mark a class #[AppTemplate], then give each copy an entry with its own ID and options:
apps:
hall-motion-light:
class: App\MotionLight
options:
motionSensor: binary_sensor.hall_motion
light: light.hall
landing-motion-light:
class: App\MotionLight
options:
motionSensor: binary_sensor.landing_motion
light: light.landingEach entry is an app of its own in the logs, make status and the store. The class can also come from a Composer
package. make app NAME=MotionLight TEMPLATE=1 writes one with its test, and the
motion light recipe has a complete one.
Typos caught before deploy. After make generate, PhpStorm completes entity IDs in getState(),
watchStateChanges(), ServiceTarget::forEntities() and the other methods that take one, and action names in
callAction(). The new stewart-php/phpstan extension makes make stan fail on a name Home Assistant does not
have, and names the closest one:
24 Entity ID "light.porhc" was not in Home Assistant at the last stewart generate. Did you mean
"light.porch"?
🪪 stewart.entityId
New projects have it. Typos caught by PHPStan
shows how to add it to an existing project.
Also new
http.metrics.listenserves/metricson a port of its own, with a token of its own or none, so Prometheus no
longer needs the token that can pause apps. The Helm chart opens it withmetrics.enabled, scrapes it with
metrics.podMonitor, and can lock the pod down with an optional NetworkPolicy.stewart checkandstewart doctorreport an admin port without a token before the daemon refuses to start, and
the admin API warns when it listens beyond this host.- Raw states have
isOn()andisOff(), and the test harness matches option names the way Stewart does.
Upgrading
Run make upgrade VERSION=1.8, then make generate, then restart Stewart. The generated classes move to a new
format; until you regenerate them, each generated call prints a deprecation line. On Kubernetes, move
adminApi.podMonitor to metrics.podMonitor. The
upgrading guide has the details, including the command that
moves your code to callAction() and how to add the PHPStan extension.
Every change is in the changelog.
v1.7.0
Stewart 1.7 helps you find out why an automation fired, or why it did not: trace one app line by line, give it a log
level of its own, and see in make status when each subscription last fired and when each schedule runs next.
Highlights
Trace an app. make run TRACE=porch-light (also make watch and make debug) logs what reaches the app and what
it does with it, at any log level:
… TRACE [porch-light] Event received {"subscription":"state_change exact:binary_sensor.porch_motion","id":"w0:3","change":"on → off"}
… TRACE [porch-light] Event received {"subscription":"state_change exact:binary_sensor.porch_motion","id":"w0:3","change":"off → on"}
… TRACE [porch-light] Handler called {"subscription":"state_change exact:binary_sensor.porch_motion","id":"w0:3"}
… TRACE [porch-light] Service call sent {"service":"light.turn_on","target":{"entity_id":["light.porch"]}}
… TRACE [porch-light] Service call answered {"service":"light.turn_on","outcome":"succeeded","took":"42ms"}
An event that is received but never reaches the handler was stopped by an operator, such as a filter() that said
no. apps.porch-light.trace: true in stewart.yaml keeps an app traced, and a reload applies it.
One app's status. make status APP=porch-light lists each of the app's subscriptions with how many events
reached it and how many got through to the handler, and each schedule and timer with its next and last run.
WATCH=1 redraws it every two seconds and keeps trying while Stewart restarts. The Apps table now shows when each
app's handler last ran and when its next schedule is due.
Every log line names its source. INFO [porch-light] …, [broker], [@shared] or [worker 0] come right after
the level, and lines Stewart writes about an app, such as App failed, carry that app's tag. apps.<id>.log_level
makes one app say more, or less, than the rest. The new Logs
guide explains the line, the JSON fields and tracing.
Also new
- An
mqttrow inmake statusand fourstewart_mqtt_*metrics show the MQTT connection, its outages and dropped
messages. Warnings for dropped messages are sampled instead of one per message. - A
generatedrow shows whether the generated classes still match Home Assistant. Stewart checks again after each
reconnect, when entities are added or removed, and after each reload, somake generatethenmake reloadclears
it. stewart status --jsonholds every subscription and schedule with their figures, and the admin API's apps gain
last_handled_atandnext_run_at.
Security
Stewart now requires PHP 8.5.12, the security release that fixes two Core advisories (GHSA-w8hw-w9fx-j79w and
GHSA-6wmf-4rv5-p3rp). The runtime image already runs it.
Upgrading
Run make upgrade VERSION=1.7, then restart Stewart. make status and the other control targets need the daemon on
1.7 too. Anything that parses line logs needs to allow for the new [source] tag. To get TRACE, APP and WATCH,
copy them from the skeleton's Makefile. The
upgrading guide has the details.
Every change is in the changelog.
v1.6.0
Stewart 1.6 lets you change apps and their settings without restarting: a reload restarts the apps on the current
code while the connection to Home Assistant stays open, and make watch reloads every time you save.
Highlights
make reload applies your changes in place. It checks the project as make check does, then restarts every
worker on the code as it is now. Edited apps, services.php and regenerated classes take effect, and Stewart neither
reconnects to Home Assistant nor fetches every state again. If the check fails, nothing changes, and make status
shows why on its new reload row:
reload failed 12s ago: App "battery-alert" sets option "treshold", which matches no parameter of App\BatteryAlert::__construct(). Did you mean "threshold"?
docker compose kill -s HUP stewart and POST /api/reload on the admin API do the same.
Settings and new apps too. A reload rereads stewart.yaml and the apps/ directory: added and removed apps,
their options, enabled, paused, worker, and workers all apply. A change to any other setting, such as
persistence or mqtt, waits for a restart, and the reload row says partly applied and names it.
make watch for development. It runs Stewart in the foreground like make run, accepts the same ONLY and
DRY_RUN, and reloads a second or two after you save a file in apps/, services.php or stewart.yaml. A save that
breaks something keeps the apps running as they were and logs the error.
Also new
- A reload does not count toward a worker's restarts, and it brings a quarantined worker back.
- After a reload removes an app, its exposed entities are deleted after the usual grace when
expose.pruneis on. stewart_reloads_total{outcome}counts reloads.
Upgrading
Run make upgrade VERSION=1.6, then restart Stewart. make status and the other control targets need the daemon
on 1.6 too. To get make reload and make watch, copy them from the skeleton's Makefile. A SIGHUP now reloads
Stewart instead of stopping it, also in Git mode. The
upgrading guide has the details.
Every change is in the changelog.
v1.5.0
Stewart 1.5 catches mistakes before they reach your server: a typo in an option, a missing setting or a broken app
now fails on your machine, in CI or at the deploy, not in the running daemon.
Highlights
make check builds every app. It wires each app the way its worker does, without running your code, and names
every app that would not start: an unknown or missing option, a constructor argument nothing provides, a Store
without persistence.url, an Mqtt without mqtt.url, or a #[PeerStore] with a typo.
App "battery-alert" sets option "treshold", which matches no parameter of App\BatteryAlert::__construct(). Did you mean "threshold"?
App "keeper" cannot be built: Persistence is not configured; set persistence.url in stewart.yaml or STEWART_PERSISTENCE__URL in .env.
The Git deploy mode runs the same check on every push, so such a commit no longer goes out with one app failing.
CI guards the image. New projects' CI also runs make check, and the Image workflow builds only after CI
passes, so a commit that fails never becomes an image a server can pull.
make deploy stays up. It installs changed dependencies and runs stewart check in one-off containers first,
and only then restarts Stewart. A failed check stops the deploy and leaves the running daemon alone.
No more empty dispose(). App now asks only for initialize(). An app that has something to close implements
the new Disposable interface; every other app can delete its empty dispose().
Shorter tests for exposed entities. requireSwitch('heater')->pushTurnOn(), pushPress(), pushOption('eco')
and pushValue(3) replace building a command by hand.
Also new
- Apps without
worker:are placed by a hash of their ID, so adding an app no longer moves the others between
workers. After the upgrade, apps can move once. make checkmentions classes inapps/that implementAppbut lack#[Automation].
Upgrading
Run make upgrade VERSION=1.5, then make check, and delete the empty dispose() methods it lists. An app that
marks dispose() with #[\Override] must implement Disposable before it loads again. The new make deploy and CI
steps come from the skeleton when you want them. The
upgrading guide has the details.
Every change is in the changelog.
v1.4.0
Stewart 1.4 is about the first hour with a new project: fewer steps to your first automation, and clear answers when
something is not set up yet.
Highlights
make doctor checks the whole path. Besides the container, it now connects to Home Assistant and tells you
whether the token belongs to an administrator, whether Home Assistant has a location for sun schedules, whether the
Stewart integration is installed, and whether the store answers. Every line says what to fix.
connection ✓ Home Assistant 2026.9.4 at ws://homeassistant.local:8123/api/websocket
token ✓ belongs to Stewart, an administrator
location ! warn Home Assistant has none, so sun schedules fail; set it under Settings → System → General
integration ! warn not installed; apps need it only to expose entities
store ✓ answers at redis://valkey:6379/0
A friendlier first app. New projects start with a short HelloApp that greets you in the log and with a
notification in Home Assistant. The bigger exposure demo lives on as ChangeCounter, switched off until you install
the integration.
make app NAME=PorchLight creates an app and its test, ready for make test.
stewart check shows where every app runs: a table of your apps with their class, the worker each one runs on,
and whether it is enabled, paused or disabled.
Valkey only when you need it. The store now runs under a Compose profile. If no app keeps data across restarts,
remove two lines from .env and Valkey stays off.
Also new
make check,make app-pause APP=…,make app-resume APP=…,make app-reset APP=…and
make worker-restart WORKER=…in new projects.- The Helm chart creates a Service for the admin API once it listens beyond loopback.
- The README starts with a Docker-only install, and the install guide covers Windows through WSL2.
Upgrading
Run make upgrade VERSION=1.4. Existing projects keep working unchanged; the new Makefile targets and the optional
Valkey profile come from the skeleton when you want them. The
upgrading guide lists the lines to copy.
Every change is in the changelog.
v1.3.0
Home Assistant can delete the devices of a daemon that is gone, an unknown app ID in the configuration warns
instead of stopping Stewart, and status reports the memory each process really uses.
Added
- Home Assistant can delete the devices of a Stewart daemon that is not connected; deleting its hub removes its
devices and entities, so a testexpose.instanceno longer lingers
Fixed
- The Helm chart rolls the pod when
homeAssistant.tokenorpersistence.urlchanges, instead of keeping the
old value until the next restart host.docker.internalreaches the host withmake upand the deploy compose files too, not only withmake runstewart statusand thestewart_broker_memory_bytesandstewart_worker_memory_bytesmetrics report each
process's resident memory, instead of the PHP heap, which left out opcache and extensions; expect higher values- An app ID in
stewart.yamlor aSTEWART_APPS__*variable that names no automation logs a warning and is ignored,
as documented, instead of stopping the daemon;stewart checkprints the warning and passes stewart --helplists the commands, instead of showing the help ofrun;stewart run --helpstill shows it- The CLI reference lists only each command's own arguments and options, instead of the console's global options
underconfig:reference - The metrics reference lists the labels of
stewart_app_exposed_commands_totaland
stewart_worker_restart_due_timestamp_seconds, instead of none - The error reference says what to do for the errors a first run or an upgrade hits: Home Assistant connection and
token, configuration files and tokens, control and worker protocol versions, and the store
Upgrading
- Run
make upgrade VERSION=1.3. Protocols are unchanged, so a 1.2 daemon,stewartCLI and Home Assistant
integration work with 1.3. - For
host.docker.internalwithmake up, addextra_hosts: ["host.docker.internal:host-gateway"]to the
stewartservice incompose.yaml, and in any deploy compose file you copied. - Memory now reads higher, since it counts the whole process; raise alerts on
stewart_broker_memory_bytesand
stewart_worker_memory_bytesto match.
v1.2.1
A burst of state changes from Home Assistant reaches workers whole again.
Fixed
- State changes of an entity merge only once a full
worker_state_batchis queued for a worker, so a burst from one
Home Assistant frame reaches handlers step by step instead of losing the changes between its first and its latest
v1.2.0
State changes cost less CPU on the broker and on workers, and Home Assistant traffic arrives in fewer, cheaper frames.
Changed
- MQTT and Valkey connections turn on
TCP_NODELAY, so a burst of small packets no longer waits on delayed ACKs - The Home Assistant client asks for
coalesce_messages, so a burst of events arrives in fewer WebSocket frames - The runtime image keeps compiled PHP in
/tmp(opcache.file_cache), so workers and console commands after the
first start without compiling the framework again; code is still revalidated when files change - A state change's
fromcomes from the worker's state cache, so it is always whatgetState()returned just before
the change - Workers receive only the new state of each change, and the broker no longer decodes Home Assistant's
old_state,
so each state change costs less CPU on the broker and on every worker - The broker encodes state changes, snapshots and history for workers with hand-written converters instead of
reflection, and waiting on one entity no longer clears every other entity's cached routing match
Fixed
stewart status,app:pause,app:resume,app:resetandworker:restartfail at once when no daemon
listens, instead of retrying the control socket for six seconds whatever--timeoutsays- A Home Assistant frame with invalid UTF-8 is logged and dropped, instead of closing the connection and forcing a
resync