Skip to content

Releases: stewart-php/stewart

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 25 Oct 12:36
v2.1.0
59155f2

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 status and the exposure error for a missing integration say it may still be loading, not only
    that it is not installed.
  • ExposureError::ComponentMissing is 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

Choose a tag to compare

@github-actions github-actions released this 23 Oct 22:41
v2.0.0
68c09a1

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=1 is now stewart 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.podMonitor no longer offer
    them.
  • stewart generate no longer asks for --allow-shrink when classes are only renamed.
  • Everything deprecated in 1.x is removed: callService() and its test stubs, dispose() on apps without
    Disposable, and ActionManifest.

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

Choose a tag to compare

@github-actions github-actions released this 23 Oct 11:55
v1.8.0
0154c67

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.landing

Each 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.listen serves /metrics on 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 with metrics.enabled, scrapes it with
    metrics.podMonitor, and can lock the pod down with an optional NetworkPolicy.
  • stewart check and stewart doctor report 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() and isOff(), 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

Choose a tag to compare

@github-actions github-actions released this 22 Oct 19:29
v1.7.0
d55a101

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 mqtt row in make status and four stewart_mqtt_* metrics show the MQTT connection, its outages and dropped
    messages. Warnings for dropped messages are sampled instead of one per message.
  • A generated row 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, so make generate then make reload clears
    it.
  • stewart status --json holds every subscription and schedule with their figures, and the admin API's apps gain
    last_handled_at and next_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

Choose a tag to compare

@github-actions github-actions released this 21 Oct 18:25
v1.6.0
0b14a57

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.prune is 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

Choose a tag to compare

@github-actions github-actions released this 21 Oct 10:03
v1.5.0
5ac2c81

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 check mentions classes in apps/ that implement App but 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

Choose a tag to compare

@github-actions github-actions released this 20 Oct 20:22
v1.4.0
586dcbb

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

Choose a tag to compare

@github-actions github-actions released this 20 Oct 14:35
v1.3.0
38f34e0

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 test expose.instance no longer lingers

Fixed

  • The Helm chart rolls the pod when homeAssistant.token or persistence.url changes, instead of keeping the
    old value until the next restart
  • host.docker.internal reaches the host with make up and the deploy compose files too, not only with make run
  • stewart status and the stewart_broker_memory_bytes and stewart_worker_memory_bytes metrics 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.yaml or a STEWART_APPS__* variable that names no automation logs a warning and is ignored,
    as documented, instead of stopping the daemon; stewart check prints the warning and passes
  • stewart --help lists the commands, instead of showing the help of run; stewart run --help still shows it
  • The CLI reference lists only each command's own arguments and options, instead of the console's global options
    under config:reference
  • The metrics reference lists the labels of stewart_app_exposed_commands_total and
    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

  1. Run make upgrade VERSION=1.3. Protocols are unchanged, so a 1.2 daemon, stewart CLI and Home Assistant
    integration work with 1.3.
  2. For host.docker.internal with make up, add extra_hosts: ["host.docker.internal:host-gateway"] to the
    stewart service in compose.yaml, and in any deploy compose file you copied.
  3. Memory now reads higher, since it counts the whole process; raise alerts on stewart_broker_memory_bytes and
    stewart_worker_memory_bytes to match.

v1.2.1

Choose a tag to compare

@github-actions github-actions released this 19 Oct 16:55
v1.2.1
a79956f

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_batch is 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

Choose a tag to compare

@github-actions github-actions released this 19 Oct 15:13
v1.2.0
2a5b83b

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 from comes from the worker's state cache, so it is always what getState() 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:reset and worker:restart fail at once when no daemon
    listens, instead of retrying the control socket for six seconds whatever --timeout says
  • A Home Assistant frame with invalid UTF-8 is logged and dropped, instead of closing the connection and forcing a
    resync