Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

0.61.0 (2026-10-29)

This release completes the Resource Lifecycle milestone (milestone 19, design). Hassette now records what happened to each service and app instead of inferring it. Hassette no longer restarts failed services in-process: a required service's failure exits the process for your supervisor to restart, and an optional one stays dead with one ERROR. A service is RUNNING only once its initialization finishes, and a failure at boot fails startup. If the event bus dies, the process shuts down without hanging. If a timed-out hook may have left an app's work running, that instance is flagged as needing a restart, and nothing replaces it until Hassette restarts. App actions report each instance's own outcome, and every app gets its full shutdown timeout at exit. The breaking changes come first; the Upgrading page has the migration steps. Instance Identity, which re-keys stored history and routes on the instance name, comes next.

Breaking Changes

  • Services are no longer restarted in-process — when a required service fails after startup, the process exits with a nonzero code and a CRITICAL line naming the reason, so run Hassette under a restart policy (Docker restart:, systemd, the HA add-on watchdog). An optional service's failure, including a FatalError, leaves it FAILED (or CRASHED) in place with one ERROR log. A failed app-owned optional service shows up only in that log until its app reloads. RestartSpec/restart_spec are replaced by a required class attribute that every concrete Service subclass must declare: an app-owned Service must set required = False, and a required service under any parent other than Hassette raises at add_child. Resource.restart() is removed. ResourceStatus.EXHAUSTED_DEAD, ResourceStatus.EXHAUSTED_COOLING and retry_at are removed from hassette, hassette-wire and hassette-client, and session rows no longer record error_type or error_traceback. (#3524)
  • Shutdown timeouts are per hook and per stage — app_shutdown_timeout_seconds, resource_shutdown_timeout_seconds and sync_executor_shutdown_timeout_seconds are removed, and a config that still sets one fails to load. Use [hassette.lifecycle] shutdown_timeout_seconds, which now bounds each hook on its own. total_shutdown_timeout_seconds now includes a fixed 20-second foundation stage (finalizing the session and draining the database), and its default rises from 30 to 45. An explicit value therefore leaves 20 seconds less for stopping apps: 30 now gives a 10-second stop stage. Give your supervisor a stop grace period of at least 60 seconds (Docker stop_grace_period, systemd TimeoutStopSec); 45 can kill Hassette before the shutdown snapshot is written. (#3403)
  • hassette run hard-exits if work outlives shutdown — after shutdown, hassette run cancels any task still alive and hard-exits if one survives 5 seconds. A watchdog hard-exits 1 second later if the process still hasn't ended, skipping atexit handlers. Either hard exit uses exit code 70 instead of 0, so your supervisor can tell a stop that cut work off from a clean one. A fatal error still exits 1, and a config error 78. hassette.server.main() is replaced by create_core(), serve() and run(). See "How shutdown is bounded" in the operating docs. (#3587)
  • TeardownReport is replaced by ShutdownOutcome — TeardownReport and TeardownCause are replaced by ShutdownOutcome and ShutdownResult (hassette.resources). ShutdownResult is ranked DONE < FAILED < TIMED_OUT < INCOMPLETE, and an outcome carries the names of tasks still running plus one reason. Resource.teardown_report is now Resource.shutdown_outcome, and RestartRefusedError.report is now RestartRefusedError.outcome. Read .outcome instead of parsing the error message. is_restart_safe now means "no work may still be running": an outcome with only diagnostic problems, such as a hook that raised but finished, is restart-safe, so code that read it as "no errors" should check result instead. Hassette.force_terminated_work and record_force_terminated() are removed. (#3587, #3550)
  • App start, stop and reload endpoints wait and report per-instance outcomes — they answer 200 instead of 202, once the action has finished. ActionResponse.status is removed, and the required ActionResponse.outcomes has one entry per targeted instance: instance_index, instance_name, result (succeeded, failed or refused), needs_restart and reason. A 409 needs_restart means an instance was already flagged as needing a Hassette restart, so the action refused it (its siblings were still acted on). A 500 action_failed now also covers a failed stop and an action that leaves an instance needing a restart. Both error bodies carry outcomes. hassette-client requires a server at API schema 4; if you retry on 409, catch the new NeedsRestartError before a generic ConflictError. hassette app start|stop|reload --json prints outcomes and no longer prints status. (#3575)
  • Removed settings — each of these is now an unknown-key ConfigError at load; delete them from hassette.toml and from HASSETTE__* environment variables:
    • strict_lifecycle. Startup registry validation always logs warnings. (#3483)
    • [hassette.websocket] authentication_timeout_seconds, response_timeout_seconds, total_timeout_seconds and cleanup_timeout_seconds. These were internal protocol timeouts and keep their old values. (#3449)
    • [hassette.lifecycle] failed_instance_cleanup_timeout_seconds. (#3550)
    • [hassette.logging] service_watcher. Hassette refuses to start (exit 78) until it is deleted. (#3524)
  • InvalidLifecycleTransitionError is removed — assigning a ResourceStatus now always applies (logged at DEBUG) instead of being checked against a transition table. Drop any except InvalidLifecycleTransitionError handlers. (#3480)

For App Authors: You Can Now Delete

  • A raised total_shutdown_timeout_seconds, or on_shutdown hooks kept artificially short, so later apps weren't cancelled at exit — every app's shutdown hooks now get their full shutdown_timeout_seconds however many apps there are. Re-check any explicit total against the new foundation stage (see Breaking Changes). (#3403)
  • A raised [hassette.lifecycle] startup_timeout_seconds to let a long database migration finish — the database startup phase now gets startup_timeout_seconds plus database.migration_timeout_seconds on its own. (#3505)
  • A raised database.max_consecutive_heartbeat_failures to stop DatabaseService restarting during long retention passes — a heartbeat failure no longer counts while writes are still completing. (#3457)
  • Warning filters or IGNORE entries for one-off HassetteBlockingIOWarning loop stalls — Tier 1 stalls now warn only when the same place stalls twice, and the blocking findings list hides one-off stalls (blocking_io.min_events). (#3439, #3441)

Lifecycle

  • A service is marked RUNNING only after its whole initialization, including its children and after_initialize(), has finished. Any service that fails to initialize during boot fails startup at once, and the fatal reason names each failed service and its error. Startup no longer waits out the full timeout. Each startup wave's startup_timeout_seconds covers initialization plus readiness. When a wave times out, the log lists each unfinished service and the line it is waiting on. (#3507)
  • When the event bus dies, Hassette now shuts down cleanly instead of blocking on a full event channel until the shutdown deadlines force-terminate everything. Every resource still reaches STOPPED. (#3537)
  • An app instance whose teardown may have left work running (a hook past its timeout, tasks still pending, a shutdown that raised) is now flagged as needing a restart. It stays FAILED, with one ERROR naming what was left running, until Hassette restarts. The flag shows in the web UI, in a Restart column in hassette app, in a hassette status table, and as needs_restart on AppInstance, AppSummary.needs_restart_count and the app_status_changed event. An instance whose init failed is now shut down through the normal path, so its listeners and jobs are removed the same way as on a stop. (#3550)
  • Start, reload, autostart and config changes never create an instance over one flagged as needing a restart. Instead they log one ERROR telling you to restart Hassette, and sibling instances start as usual. Config and file-change reloads now decide each app's plan while holding its lock, so a stop that finishes mid-reload can no longer bring back the instances it just stopped. (#3566)
  • An app hook cut off by its timeout now leaves the instance FAILED with a "timed out" reason instead of STOPPED, even if the hook catches the cancellation and returns. (#3403)

Blocking I/O Detection

  • The Tier 1 loop watchdog warns only when the same place (app plus innermost app frame, or the handler or job) stalls the loop a second time. The first stall is still recorded in telemetry and logged at DEBUG. (#3439)
  • New [hassette.blocking_io] min_events (default 2) hides Tier 1 findings with fewer events from the findings list. Set it to 1 for the old behavior. Tier 2 findings are never hidden. The Apps list's blocking badge counts the same findings, so it no longer shows for an app whose findings list is empty. (#3441, #3445)

Scheduler

  • Scheduled jobs store next_run, fire_at and jitter in the database at registration and keep them current after each fire and reschedule. Rows for jobs not registered this session are cleared, so DB-backed (degraded) responses, the UI and the CLI no longer show stale or phantom timing. (#3407, #3411, #3415)
  • Jitter is rolled when a job's next run is set, so fire_at is final before the job is registered or persisted. (#3400)
  • A listener or job registered at runtime (for example run_in(..., if_exists="replace") from a handler) no longer re-parses the source file on the event loop each time. (#3372)

Database & Telemetry

  • A busy database worker is no longer restarted during a long retention pass, and a genuinely stalled one is caught in about 90 seconds instead of about 15 minutes: the database.heartbeat_interval_seconds default drops from 300 to 30. (#3457)
  • Fire-and-forget telemetry writes no longer starve awaited database writes. (#3453)
  • Slow database migrations can finish within database.migration_timeout_seconds at startup, and a migration timeout now names that setting. (#3505)
  • The default log view uses a new partial index, so it stays fast when app lines are sparse among framework lines. (#3381)

Logging

  • Stack traces, tracebacks and structlog lines keep their shape on the background log queue. Previously, stdlib tracebacks were folded into the message, and some native structlog lines were lost with --- Logging error ---. JSON output uses one stack and one exception key. Stored log rows keep the traceback separately, so the dashboard log drawer's exception panel shows it. hassette log and hassette execution tables show the exception's Type: message line under the message. (#3220)
  • Stdlib log lines are stamped with the time they were created, not the time the log queue rendered them, so lines no longer reorder under load. (#3486)
  • A failed service initialization logs its traceback once instead of three times. (#3508)

Bug Fixes

  • Bus, scheduler and websocket invariant checks now raise explicit errors instead of using assert, so they still fire under python -O. (#3378, #3380, #3382)
  • Database error handlers catch only storage errors, so a programming error now surfaces instead of being logged as a storage failure and swallowed. (#3463, #3488, #3490)
  • The CLI rejects a server_url with user:password@ userinfo when a Hassette token is also configured, instead of silently dropping the token and failing authentication. (#3430)
  • The Docker latest tag only moves for the highest stable release, so a patch to an older release line no longer moves latest backwards. (#3423)

Documentation

  • The operating guide explains how shutdown is bounded, error handler registration, the shutdown snapshot and Docker grace period, instances that need a restart, HA-restart, reconnect and degraded-database handling, and how to pick and update a pinned Docker image tag. (#3512, #3555, #3556, #3566, #3587)
  • The AppDaemon migration guide clarifies app config brackets, when the state cache is populated, handler-signature imports, the forgotten-await claim, emit, the missing-name error and testing examples. It also says to disable the AppDaemon app before deploying its Hassette migration. (#3564, #3569, #3572, #3578, #3579, #3580, #3583)
  • The lifecycle and internals pages were corrected: the CRASHED event vs status, when start()/initialize() check the shutdown outcome, StateProxy's event source and listener priority, scheduler missed jobs, and cleaning up app-owned resources when on_initialize fails. (#3402, #3517, #3528, #3530, #3540, #3576)
  • Bus, scheduler and state docs: glob topics for bus.on(), mode= on every subscription method, execution-mode guidance, scheduler mode values, every accessor in the predicate reference, Topic in the raw-event example, the climate attribute and debounce sensor recipes, testing the daily notification recipe, and custom state value types. (#3467, #3469, #3478, #3487, #3489, #3525, #3529, #3531, #3532, #3535, #3538, #3588, #3590)
  • Logging and configuration docs: where an app's logs go, every setting needed to see an app's framework debug lines, the log-level and blocking-I/O tables, file-watcher log levels, backpressure, the app shutdown hook timeout, and restarting after setting trusted_proxies. (#3377, #3379, #3385, #3388, #3545, #3554, #3560, #3585)
  • Getting started, Docker and CLI docs: upgrade steps, the first automation's verify step, Docker timezone, compose and troubleshooting snippets, the add-on example app, running the CLI, homeassistant_started ordering, troubleshooting setup steps, and test harness identifiers and examples. (#3383, #3393, #3396, #3398, #3482, #3485, #3533, #3542, #3547, #3549, #3557, #3558, #3562)
  • Public api, utils, bus, scheduler, trigger, sync facade, task bucket, history, entrypoint and config methods now have docstrings. (#3384, #3387, #3410, #3416)

0.60.0 (2026-10-22)

This release completes the Log Identity milestone (milestone 17, design). Every app instance now has one name you choose, and that name drives its logger names and owner identity. Every log line records who wrote it, which app it is about, and which handler or job run produced it, as separate facts. Log views default to your apps' lines plus Hassette's errors. The rest are one opt-in away, and framework lines are marked. The milestone's breaking changes are listed first; the Upgrading page has the migration steps. Instance Identity, which re-keys stored history and routes on the instance name, comes next.

Breaking Changes

  • instance_name is required and unique — a configured app instance without instance_name now fails at config load, and so does a duplicate name anywhere in the config. A valid name starts with a lowercase letter, contains only lowercase letters, digits and _, doesn't end in _, and is at most 128 characters. Old generated defaults like MyApp.0 are gone, so cache keys, event source comparisons and logger names built from them change. AppTestHarness now requires a config that sets instance_name. Add instance_name to every [hassette.apps.<key>] entry (and every multi-instance entry) and update anything that matched the old names. (#3146)
  • App loggers are now hassette.apps.<instance_name> — lifecycle lines about an app (starting, stopping, hook failures, readiness) come from hassette.apps.<instance_name>.Lifecycle, follow logging.app_handler instead of the app's log_level, and are marked framework rather than app. Set logging.app_handler = "DEBUG" to see an app's lifecycle detail at DEBUG. Update any logging config or filters that target the old logger names. (#3173, #3146)
  • source_tier on log records is now author — LogEntry.source_tier (wire, GET /api/logs/recent, GET /api/executions/{execution_id}) is now LogEntry.author, "app" or "framework". The source_tier query parameter on the log routes is replaced by author (all, app or framework); a stray source_tier is ignored and the default view applies. HassetteClient.get_recent_logs(source_tier=...) is now author=..., get_execution_logs accepts author, and the client requires API schema 3. hassette log --source-tier is now --author, and hassette execution accepts --author. Existing log history keeps its meaning through an automatic one-way migration. (#3197)
  • Log views hide Hassette's routine lines by default — with no author, log queries, hassette log, and every web UI log view return app-authored lines plus framework lines at ERROR and above, where they previously returned everything. Pass author=all (API and client), --author all (CLI), or tick "all framework lines" in a log view's footer (web UI) to see the rest. (#3197)
  • App autodetection is removed — apps.autodetect and apps.exclude_dirs are gone, so HASSETTE__APPS__AUTODETECT and HASSETTE__APPS__EXCLUDE_DIRS now fail startup as unknown keys; delete them. Every app needs a [hassette.apps.<key>] entry, including apps built on a shared base class, or it won't run. An autodetected app's history stays under its old key (<apps dir name>.<module path>.<ClassName>, e.g. apps.lights.LightsApp, the key the Apps page shows) and is listed as not in the current config until retention clears it. To keep that history, reuse the old key as a quoted table name, e.g. [hassette.apps."apps.lights.LightsApp"]. The auto_loaded field in app API responses is deprecated and always false; it will be removed in a future release (#3161). (#3158)

For App Authors: You Can Now Delete

  • Raising logging.bus_service, logging.scheduler_service and similar globally to debug one app — set framework_log_level on that app's config to see Hassette's lines about just that app. (#3178)
  • Workarounds for get_state_value() and get_attribute() under AppTestHarness — code that was rewritten to call get_state(), or tests that hand-mocked those two calls, can go back to the natural calls, async or via self.api.sync. (#3132)
  • Hand-mocked get_state_raw() and get_states_raw() in tests — seed the entities and read them through RecordingApi. (#3130)

Logging

  • Each log line now records its author (your app's code, or Hassette), its subject (the app instance it is about) and its execution (the handler or job run) independently. Author comes from the logger that wrote the line, never from the subject, so Hassette's lines about your app (lifecycle, hook failures, executor errors) are no longer shown as the app's own output. Warnings raised by Hassette packages are framework-authored; your own warnings stay yours. (#3173)
  • Console log lines drop the key=value tail and print one identity column before the message: the instance name, hassette:<instance_name> when the framework wrote a line about that app, or the logger name for lines with no app. During a handler or job run it ends in @ plus the last 8 characters of the execution ID, the same short ID the web UI shows. Everything the column drops is still in JSON output and the telemetry database. (#3203)
  • New log_format = "compact" prints the console layout with an HH:MM:SS timestamp and no level padding. The Home Assistant add-on uses it so the Supervisor Log tab reads on a phone. auto never picks it. (#3203)
  • New framework_log_level on AppConfig sets the level for every Hassette logger one app instance owns: its lifecycle logger and its Bus, Scheduler, Api, StateManager and TaskBucket loggers. log_level still governs only the app's own logger. Unset, nothing changes. Shared services such as CommandExecutor have no owning app and keep their logging.* levels. (#3178)
  • Web UI: the All / Apps / Framework toggle and its ?tier= URL parameter are replaced by one persisted "all framework lines" checkbox shared by every log view. Framework-authored lines carry a "Framework" badge, and the CLI log tables gain an Author column. (#3197)
  • Docs: the log-levels page explains which logger and level field governs each kind of line about an app, how to read the console identity column, restarts, and the API port. (#3182)

Apps & Configuration

  • At startup, Hassette scans the apps directory without importing anything and warns about each App subclass no entry configures, with a paste-ready [hassette.apps.<key>] section. Suggested keys avoid configured keys and each other, so pasting every snippet gives a config that loads. Set apps.warn_unconfigured = false to turn it off. (#3158)
  • A live config reload that moves an instance name between apps now stops everything it replaces before creating anything, and a reload into a config with duplicate names is rejected while the running config stays in place. (#3146)

Test Utilities

  • RecordingApi supports get_state_value() and get_attribute() (with dot paths, returning MISSING_VALUE for absent attributes), and get_state_raw() and get_states_raw(), which return deep copies of the seeded state. All raise EntityNotFoundError for unseeded entities. (#3132, #3130)

State Models

  • self.states.schedule and self.states.group resolve to typed models (ScheduleState with next_event, and GroupState for legacy group.* entities) instead of BaseState. (#3131)

CLI

  • Status and health values in the hassette status, app, app activity and dashboard tables are colored on a terminal and plain when piped; --json is unaffected. (#3126, #3150)
  • hassette log tables keep their When, Level, App, Instance and Function columns readable when a message spans several lines, such as a traceback. The Message column now wraps instead of squeezing the others to nothing. (#3147)

Documentation

  • Clarified CLI global flag placement, log persistence, --dev-mode, reload settings, per-app developer overrides, heartbeat timing, and where the web UI token lives. (#3171, #3179, #3159)
  • Corrected which call_service failures wait_for_ack surfaces (a nonexistent entity ID is only logged by Home Assistant), described get_state() .value as a converted, domain-typed value, fixed the get_attribute dot-path example, and showed the full AppSync cover example. (#3165, #3180, #3202)
  • Fixed the First Automation test steps and expected log output, split the cross-app bus example into real modules, and corrected the scheduler migration page's examples. (#3181)
  • Added zsh fpath setup so shell completion loads. (#3183)

0.59.0 (2026-10-19)

Breaking Changes

  • role is gone from the web API's service status — GET /api/health no longer includes services[].role, hassette_wire.ResourceRole and OpenResourceRole are removed, and hassette.types.ResourceRole drops its unused CORE, BASE and UNKNOWN members. Clients built before this release require role and can't parse /api/health: upgrade hassette-client to 0.59.0, and update the hass-hassette integration to a release built on it before or alongside this upgrade (v0.2.0 and earlier fail to connect). (#3063)
  • Scheduler delays and intervals must be at least 1 second — run_in() and After() raise ValueError for a total delay under 1 second, and run_every() and Every() for an interval under 1 second. The scheduler fires on whole seconds, so a sub-second delay used to fire anywhere from immediately to about 0.8 s later. Use a delay of 1 second or more, or await asyncio.sleep() inside a handler for a shorter pause. Fractional jitter is still allowed. (#3077)
  • Resource.cleanup() takes no arguments — the timeout parameter on Resource.cleanup() and the App and DatabaseService overrides is removed. Shutdown already bounds cleanup with cleanup_timeout, so it did nothing. A custom Resource or Service override should become async def cleanup(self) -> None calling await super().cleanup(). Passing the old argument, as in super().cleanup(timeout), raises TypeError, which shutdown records as a failed cleanup. The unused LifecycleMixin.initializing and LifecycleMixin.task properties are also removed. (#3117)

For App Authors: You Can Now Delete

  • Custom state classes for wake_word, infrared, radio_frequency, alert or tag entities — these domains now have typed models (self.states.tag, self.states.alert, and so on) instead of falling back to BaseState. (#3103)
  • Raising logging.web_api to WARNING just to silence per-request access lines — access lines are now logged only when logging.web_api is DEBUG, so the default no longer fills the log with healthcheck and integration polling. (#3050)

Bus, Scheduler & Apps

  • on_state_change and on_attribute_change log a WARNING when an app registers a literal entity ID that isn't in the state cache, naming the closest known ID when there is one. A typo used to mean the handler silently never fired. The listener still registers, and globs, framework listeners and an empty cache never warn. (#3071)
  • Typed state models for wake_word, infrared, radio_frequency and tag (timestamp state of the last event, with tag_id and last_scanned_by_device_id on tag) and alert (string state, plus an alert entity wrapper). (#3103)
  • Apps can declare an optional one-line description (up to 200 characters) in their manifest. (#3116)

Web API & Telemetry

  • GET /api/apps and GET /api/apps/{app_key} return each app's description, or null when it's unset. (#3116)
  • Listener summaries include last_error_ts, the time of the most recent error, as job summaries already did. (#3118)

Logging & Add-on

  • Uvicorn's per-request access lines are logged only when logging.web_api is DEBUG. Startup and error lines still follow logging.web_api as before. (#3050)
  • Under the Home Assistant add-on, the first-run "generated token" log line points at the sidebar's Open Web UI button instead of http://127.0.0.1:8126, which isn't reachable there, and notes the token is only needed for the CLI or a published port. (#3047)

Documentation

  • What an app reload re-imports: the Reload button, the CLI and automatic reloads re-import only the app's own module file, a config-only change restarts instances with the already-loaded code, and edits to sibling or helper modules need a Hassette restart. (#3094)

0.58.0 (2026-10-17)

Breaking Changes

Run hassette run --check after upgrading. Removed settings fail it as unknown keys, with the closest real setting suggested.

  • A path in base_url is kept as a prefix — REST requests go to <base_url>/api/ and the WebSocket to <base_url>/api/websocket, so Home Assistant served under a sub-path (or the add-on's http://supervisor/core) now works. A base_url with a leftover page path, such as a browser URL like http://ha.local:8123/lovelace/0, now connects under that path and fails. Remove the path, or keep it only if a reverse proxy serves HA there. A lone / or trailing slash isn't a prefix. The resolved REST and WebSocket URLs are logged at startup. See base_url Paths in the upgrading guide. (#2974)
  • Config settings removed or given one meaning — (#2784, #2783)
  • apps.extend_exclude_dirs is removed. Move its entries to apps.exclude_dirs, which now adds to the built-in exclusions instead of replacing them.
  • logging.all_events is removed. Replace all_events = true with all_hass_events = true and all_hassette_events = true. Events are now classed by payload type, so all_hassette_events also covers topics apps publish with Bus.emit.
  • A logging.log_persistence_level stricter than logging.log_level is now a configuration error instead of being silently clamped. Unset, it defaults to log_level.
  • lifecycle.registration_await_timeout and web_api.job_history_size are removed. Nothing read them; job history is governed by database.retention_days and database.max_size_mb.
  • App entries must use filename and class_name. The file_name, class, module and module_name spellings are no longer accepted, and an entry using them is dropped with the "missing required keys" warning.
  • Retention defaults are 30 days, so lowering retention_days alone fails validation — database.retention_days (was 7), database.framework_retention_days (was 1) and logging.log_retention_days (was 3) all default to 30, so logs and framework history no longer expire before the executions they belong to. The other two must stay at or below retention_days, so a config that sets only retention_days = 14 now fails. Lower all three together. (#2814)
  • Legacy environment variable spellings are no longer read — LOG_LEVEL and HASSETTE_LOG_LEVEL become HASSETTE__LOGGING__LOG_LEVEL (the old names never set the level past bootstrap anyway), HASSETTE_CONFIG_DIR becomes HASSETTE__CONFIG_DIR, and HASSETTE_DATA_DIR becomes HASSETTE__DATA_DIR. (#2777)
  • REST failures other than 404 raise HttpRequestError — REST-backed Api methods raised a bare aiohttp.ClientResponseError for statuses such as a 503 during an HA restart, which apps catching HassetteError missed. Catch HttpRequestError (a HassetteError carrying status, message, method and url) instead. 404s still raise EntityNotFoundError, and retries are unchanged. (#2939)
  • FalseySentinel is a closed Enum — MISSING_VALUE, NOT_PROVIDED and ANY_VALUE behave as before (is checks, falsiness, isinstance, pickling), but FalseySentinel("NAME") now raises ValueError and subclassing raises TypeError. Define your own Enum with a __bool__ returning False if you need a custom falsey sentinel. Pyright can now narrow is checks against individual members. (#2834)
  • Hassette requires whenever 0.11 (>=0.11,<0.12) — apps that use whenever directly must move off the APIs 0.11 deprecates or removes: the system-timezone helpers become the SYSTEM_TZ spellings, timestamp_nanos() becomes timestamp(unit="nanosecond"), ZonedDateTime.tz becomes tz_id, and Time.py_time / Date.py_date become to_stdlib(). (#2779)
  • CLI usage errors exit with code 2 — an unknown option, missing argument or bad value now exits 2 instead of 1, so scripts can tell a usage error from a command that ran and failed. (#3022)

For App Authors: You Can Now Delete

  • Converting minutes or hours to seconds for run_in (run_in(func, 5 * 60)) — write run_in(func, minutes=5) or hours=. (#2808)
  • Handlers that store the previous reading to detect a threshold crossing — use changed=C.CrossedAbove(threshold) or C.CrossedBelow(threshold), which fire only on the transition. (#2943)
  • A separate on_attribute_change listener that reschedules an EntityTime job because attribute= ignored attribute-only changes — EntityTime(..., attribute=...) now follows the attribute itself. (#2978)
  • Reaching into AppTestHarness._harness to send an event the simulate_* helpers don't build — use harness.simulate_event(event). (#3006)
  • A hand-rolled asyncio.Semaphore around task_bucket.spawn() — pass max_tasks= to the TaskBucket. (#2833)
  • cast() or a pyright ignore on a call_service target that mixes one ID with a list — target is typed Mapping[str, str | list[str]], so {"entity_id": "light.kitchen", "area_id": ["hall", "den"]} type-checks. (#2832)
  • Deleting or moving .venv / node_modules out of a bind-mounted Docker project so the entrypoint's copy wouldn't fail on them — the copy now skips environment dirs at the project root and caches at any depth. (#2800)

Home Assistant Add-on

Hassette can now run as a Home Assistant add-on. Add https://github.com/NodeJSmith/hassette-addon as a custom repository; the add-on guide covers install, configuration and troubleshooting. (#3028)

  • The add-on image, ghcr.io/nodejsmith/hassette-addon, builds on the hassette image with a glue entrypoint and a HEALTHCHECK on /api/health/live. It connects to HA through the Supervisor proxy with no token pasting, seeds an empty /config with a commented hassette.toml and an example app, and pins the settings the add-on owns so hassette.toml can't move the listener, the HA connection, the token file or the database. Log lines render as plain text in the Log tab. (#3002, #3028)
  • The python_packages option is installed on every start, with the same constraints and conflict handling as the other installs. (#3003)
  • Dependency installs that can't reach the network at boot retry offline from the uv cache. If that fails too, hassette starts anyway with a banner listing each install that didn't run. Conflicts and 404 dependency URLs still halt. (#2960)
  • Home Assistant backups carry a consistent database snapshot taken by a backup_pre hook, and a restore puts it back on start. The new database.shutdown_snapshot_path setting writes the same snapshot on clean shutdown, so a backup of a stopped add-on is current. (#3008)
  • The add-on announces itself to the hass-hassette integration through Supervisor discovery, so a "Discovered" card appears with no connection details to enter, and every start re-announces a rotated token. Needs hass-hassette 0.2.0. (#3025)
  • New web_api.token_file setting chooses where the server reads and writes its generated token (default <data_dir>/.web_api_token). The add-on points it at /config so the token is reachable from File Editor or Samba, and the CLI reads the same path. (#3001)

Bus, Scheduler & Apps

  • C.CrossedAbove(threshold) and C.CrossedBelow(threshold) fire only when a value crosses the threshold, not on every update past it. A missing or non-numeric old or new value never counts as a crossing. (#2943)
  • run_in takes keyword-only minutes= and hours= alongside its positional delay in seconds; the parts add up. (#2808)
  • EntityTime with attribute= reschedules when that attribute changes. Each change to the watched value replaces the pending run, as HA's own time trigger does, so a positive offset on an entity that names "the next occurrence" (sun.sun's next_* attributes, a next-alarm sensor) never fires; the docs say so and suggest a negative offset or an input_datetime. (#2978)
  • TaskBucket takes an optional max_tasks that bounds how many spawned coroutines run at once. spawn() stays synchronous. (#2833)
  • New AnyEvent alias (Event[EventPayload[Any]]), exported from hassette.events, types event.payload in handlers that accept any event. A handler annotated AnyEvent | None gets the event injected like Event | None. (#2807)
  • Cancelling a listener registration mid-add no longer leaves its name reserved, so registering the same name again works instead of reporting a duplicate. (#2829)
  • StateComparison and AttrComparison raise TypeError when passed a condition class such as Increased instead of an instance. Before, they logged a warning and instantiated it. (#2828)
  • An app whose class fails validation, such as an invalid base-class order, now fails the startup precheck and is marked failed when started, and the precheck log includes the error. Before, it slipped past the precheck. (#2819)

Api, States & Errors

  • User-facing errors end with a Suggestion: line saying how to fix the problem: base_url errors, a rejected token, dependency-injection errors (with a docs link) and the state-registry domain errors. (#2809)
  • State conversion that falls back to BaseState now logs a WARNING naming the entity, the class that failed and the error. It was logged at DEBUG, so a broken state model only showed up as a plain BaseState. (#2912)

Telemetry & Retention

  • Retention cleanup deletes old sessions rows once nothing references them. Before, every start left a row behind forever. (#2812)
  • An app removed from config disappears from the app list, and from the HA integration's entities, once its history has aged out of retention. Until then it's listed as "not in current config". (#2761)
  • GET /api/logs/recent takes an optional instance_index to scope a multi-instance app's logs to one instance. (#2940)
  • Listener and job summaries include last_error_execution_id, the execution behind the most recent error. (#2942)

Startup, Logging & Docker

  • The startup line includes the running version and the time since hassette was imported, e.g. Starting Hassette v0.58.0... (0.42s since hassette import). (#2986, #2805)
  • A default-level hassette run shows only the config, apps, web API and HA connection lines. Startup internals log at DEBUG, "Found 0 inactive apps" is suppressed, and the default non-loopback web API bind logs at INFO instead of warning. The logging service's shutdown notice logs at DEBUG. (#2821, #2810)
  • A trusted_proxies hostname that doesn't resolve at startup no longer stops Hassette. It logs a warning, trusts nothing from that entry, and the refresh job adds its addresses once DNS answers. Malformed IP/CIDR literals and match-all ranges still fail startup. (#2941)
  • The Docker image ships precompiled bytecode, roughly halving cold-start import time (about 3.8 s to 2.0 s in testing) for a 10 MB larger image. (#2805)
  • User dependency installs in Docker can no longer downgrade hassette's own dependencies; a pin like pydantic<2 now stops with the DEPENDENCY CONFLICT banner instead of producing an image that can't import hassette. (#2973)
  • An unreachable package index is reported as a network error instead of "DEPENDENCY CONFLICT", and stopping the container exits 0 in every phase, including during a dependency install. (#2960)

Web UI & CLI

  • hassette job trigger <id> fires a scheduled job once, right now. (#3004)
  • In --json mode, a command against a non-loopback target prints the Target: line, and the TLS warning when verification is off, to stderr. stdout still carries only the JSON. (#2816)
  • The CLI's auth-failure messages name the token source web_api.token_file. (#3001)
  • Config help popovers render docstring markup as readable text instead of literal backticks and :class: roles. (#2813)
  • Copying from the app Code tab pastes one line per source line instead of one per token. (#2910)
  • The sidebar's RUNNING group stays open when stopped apps exist, and can be collapsed by hand. (#2911)
  • App lifecycle transitions no longer appear in the Diagnostics Services panel, where instances of the same app class overwrote each other. (#2913)
  • Buttons share one set of styles, so a few controls shift slightly: hamburger and close icons are 16px, secondary text actions underline on hover only, and "Log in to Hassette" is an outlined button. (#2972)

Documentation

  • New Home Assistant add-on section: setup, configuration, and watchdog, backups and troubleshooting. (#3028)
  • README and docs landing page rewritten around one working motion-lights app, with the hass-hassette integration and a "Coming from AppDaemon?" section. A new "How Hassette Is Built" page explains how AI agents write the code and how it's checked. (#2785, #2820)
  • The motion_lights example and the debounce recipe no longer turn a light off when motion returns; they use duration= instead of debounce=. (#2797, #2785)
  • Scheduling relative to sunrise and sunset with EntityTime. (#2957)
  • Which state-conversion paths fall back to BaseState and which raise. (#2937)
  • call_service's target= and targeting keys in **data are equivalent; target= is preferred. (#2827)
  • Terms defined on first use in the API methods, Apps and Task Bucket pages, and corrected hassette app sample output in Getting Started. (#2998, #2980)

0.57.0 (2026-10-13)

Breaking Changes

Migration steps for the configuration items are in the upgrading guide. Run hassette run --check after upgrading.

  • Hassette no longer exits when it can't reach Home Assistant — it reconnects in place and apps keep running against the last known state cache until HA is back. Apps that relied on the process restarting after an HA reboot to rerun on_initialize must handle websocket_connected (or homeassistant_started) instead. The [hassette.websocket] retry settings (connect_retry_*, early_drop_*, max_recovery_seconds) are removed and, since unknown keys now fail startup, must be deleted from your config. CouldNotFindHomeAssistantError and the TEST_EARLY_DROP_* testing constants are removed, and InvalidAuthError is now a HassetteError rather than a FatalError. (#2743)
  • Unknown settings fail startup, and several config paths and names moved — see the groups below. (#2641)
  • Unknown settings: an unknown HASSETTE__* environment variable or hassette.toml key stops startup with exit code 78. The error names the key and suggests the closest real setting. HASSETTE__LOG_LEVEL is now HASSETTE__LOGGING__LOG_LEVEL.
  • Config locations: config_dir drives file lookup and can't be set in hassette.toml or .env; pass --config-dir or set HASSETTE__CONFIG_DIR in the process environment. Relative paths in a config file are relative to that file's directory, not the working directory. A config location you pass explicitly that doesn't exist is an error. The config_file and env_file settings are retired; use the --config-file and --env-file CLI flags.
  • App config: an AppConfig env_prefix that starts with hassette__ is now an unknown-key error because that namespace is reserved. Rename it to something like MYAPP_. AppConfig reads the same .env files as the running Hassette config instead of the fixed /config/.env, ./.env and ./config/.env list; set env_file in the app's model_config to read a specific file. App config overrides through HASSETTE__APPS__<APP_KEY>__CONFIG__<FIELD> keep working.
  • Exit codes and logging: a missing HA token and other config errors exit with code 78, so supervisors that restart on any non-zero exit should treat 78 as "edit the config first". hassette run logs to stdout; update log collection that read stderr.
  • Docker: apps moved from /apps to /config/apps and the image no longer sets HASSETTE__APP_DIR. Move ./apps to ./config/apps and drop the ./apps:/apps volume, or keep the mount and set HASSETTE__APPS__DIRECTORY=/apps. The docker_start.sh variables were renamed with no fallback: HASSETTE__INSTALL_DEPS, HASSETTE__PROJECT_DIR and HASSETTE__PRUNE_UV_CACHE are now HASSETTE_DOCKER_INSTALL_DEPS, HASSETTE_DOCKER_PROJECT_DIR and HASSETTE_DOCKER_PRUNE_UV_CACHE.
  • run_app_precheck and disable_state_proxy_polling removed — both now fail startup as unknown keys. The app precheck always runs; set allow_startup_if_app_precheck_fails = true to start despite a failure. The state cache poll always runs; tune it with state_proxy_poll_interval_seconds. (#2762)
  • scheduler.min_delay_seconds, scheduler.max_delay_seconds and scheduler.default_delay_seconds removed — they only tuned an internal sleep clamp that made jobs fire late, and have no replacement. Delete them from hassette.toml and HASSETTE__SCHEDULER__* environment variables, or startup fails with the unknown-key error above. behind_schedule_threshold_seconds is unchanged. (#2726)
  • GET /api/logs/recent validates source_tier as a case-sensitive enum — app | framework | all, defaulting to all, matching the telemetry endpoints. Values that used to be lowercased server-side, such as source_tier=APP, now return 422. (#2603)

For App Authors: You Can Now Delete

  • A hand-rolled replay guard for event.* entities after an HA restart — comparing the entity's timestamp state against last_changed or the clock inside a handler or should_run(). Use where=P.EventEntityFresh(max_age=...). (#2631)
  • changed_to= / changed_from= callables or numeric string literals written because True and 20 never matched — a lambda like lambda v: v == "on" for a boolean, or "20.0" for a number. changed_to=True now matches "on" and changed_to=20 matches "20.0". (#2684)
  • Reads of climate and water heater setpoints out of attributes.extras (temperature, target_temp_high, target_temp_low, target_temp_step) — use the typed target_temperature, target_temperature_high, target_temperature_low and, on water heaters, target_temperature_step fields. (#2635)
  • Re-arming a one-shot job from its own callback through a different name or a deferred task — run_in(..., name=<same name>, if_exists="replace") now works from inside the job's own callback. (#2650)
  • A supervisor restart policy kept only so Hassette comes back after a Home Assistant restart (Docker restart:, a systemd unit, the add-on Watchdog) — Hassette now reconnects on its own. Keep the policy if you want it for crashes. (#2743)

Home Assistant Connection

  • One reconnect loop now paces every failure. Refused connections, DNS failures and proxy 502/404 responses retry fast, with 1/2/4/5 s backoff plus jitter. A proxy's 401/403, a rejected certificate and a 429 (honoring Retry-After) retry every 5 minutes. A rejected token is retried 3 times, 60 s apart, because HA's token check is sensitive to clock skew at boot, and then the connection stays in AUTH_FAILED without reconnecting until Hassette restarts. (#2743)
  • A connection reports as connected only once HA's core state is RUNNING, so apps never see a "connected" HA that is still booting. Events during the gap aren't replayed, and the state cache is replaced on reconnect. (#2743)
  • Outage logging is bounded at about 13 rows an hour: a WARNING on the first failure, a "still unreachable" summary every 5 minutes, INFO on recovery with the outage duration, and DEBUG for each attempt. Until the first successful connection, failures log at ERROR with a base_url hint. (#2743)
  • The WebSocket's max_msg_size is raised to 16 MiB so a large get_states payload no longer triggers a close loop. (#2743)
  • A clean shutdown no longer logs task ws:recv crashed at ERROR (#2759), and request_shutdown() logs at DEBUG instead of INFO once per app instance and service (#2758).

Bus

  • Literal equality in predicates coerces Home Assistant's string state to the literal's type before comparing: ValueIs, StateFrom/StateTo/AttrFrom/AttrTo, and the changed_from= / changed_to= kwargs. C.Comparison("==", True) now agrees with changed_to=True. Use the new C.Exact(value) when you need a strict match against the raw value. (#2684)
  • P.EventEntityFresh(max_age=...) drops stale replays of event.* entities, such as buttons and remotes, that HA re-broadcasts after a restart. It compares the entity's timestamp state with its last_changed, never reads the current clock, and fails open when either value is missing or unparseable. (#2631)
  • wait_for cancellation warnings are rate-limited to one per 60 seconds per Bus outside shutdown, with a demoted count in the next warning. A WARNING now fires when pending wait_for calls reach 80% of lifecycle.max_concurrent_dispatches, and hassette status and GET /api/health report pending_wait_for_count. (#2744, #2723, #2725)
  • Removing a listener through the sync facade (bus.sync.remove_listener / remove_all_listeners) now cancels a pending wait_for on the event-loop thread, so the waiter always wakes. (#2687)

Scheduler

  • Jobs fire at their due time. The loop sleeps exactly until the next due job instead of flooring the sleep, which delayed jobs by up to 1 s in production and 2 s in dev. (#2726)
  • After accepts hours=, matching Every. Combining timedelta= with seconds= / minutes= / hours= now raises ValueError; before, the unit arguments were silently dropped. (#2648)
  • A single / queued / restart job that replaced itself from its own callback with if_exists="replace" used to hang forever. It now works for async and sync callbacks. (#2650)
  • Scheduler.remove_all_jobs() no longer raises TaskBucketSealedError during a forced shutdown. The removal methods are typed as asyncio.Future[None] rather than asyncio.Task; awaiting callers are unaffected. (#2660)
  • A handler or job that raises TimeoutError itself, such as a wait_for timing out or an aiohttp request timeout, is recorded as an error with a traceback instead of timed_out. timed_out now means the framework's execution deadline fired. (#2661)

State Models & Configuration

  • Climate and water heater setpoints populate their typed fields from Home Assistant's wire keys (temperature, target_temp_high, target_temp_low, target_temp_step). They were always None before, with the values only in attributes.extras. The values no longer appear in extras, so code reading them there must switch to the typed fields. (#2635)
  • A typed AppConfig subclass now warns when an app's hassette.toml config has keys it doesn't declare, naming the app and suggesting the closest declared field. (#2585)
  • display_name set on an app in hassette.toml is honored instead of silently dropped. (#2722)
  • Missing public types are exported from their package __all__ lists: TypedStateChangeEvent and TypedStateChangePayload from hassette.events and hassette, FalseySentinel from hassette.const, BlockingIOBehavior and ForgottenAwaitBehavior from hassette.types, and BlockingIODetectionConfig from hassette.config. HassetteNotInitializedError now subclasses HassetteError, and still subclasses RuntimeError. (#2602)

Web UI & CLI

  • The web UI can be served under Home Assistant ingress (/api/hassio_ingress/<token>/, which the add-on's sidebar panel needs) and under sub-path reverse proxies such as https://example.com/hassette/. The base path comes from X-Ingress-Path or X-Forwarded-Prefix, honored only from a trusted_proxies peer with auth on. (#2720)
  • The connection indicator says "Can't reach server" and shows the URL when the WebSocket has never connected after five attempts. Reconnect delays are jittered so open tabs don't reconnect at the same moment. (#2760)
  • The Diagnostics page refreshes system health when the HA connection drops or recovers. (#2614)
  • Start and Reload are hidden for app instances removed from config, with a "not in config" chip on the instance. (#2653)
  • A start, stop or reload request for an app that already has an action running gets an immediate 409 action_in_progress instead of queueing behind it. Re-pressing reload after a client timeout no longer starts a second full restart. (#2612)
  • Execution rows that can't open a detail view are skipped in keyboard tab order (#2643), and the sidebar's warn group is labeled WARNING instead of SLOW (#2637).
  • Piped CLI tables stay greppable: with no TTY and no $COLUMNS, tables render at 1000 columns instead of being ellipsized at 80. hassette log --instance is hidden from --help and completion. (#2645)
  • hassette status shows app_count as "N tracked instances", since it counts tracked app instances rather than configured apps. (#2629)

Bug Fixes

  • Database size limit uses live data — the size failsafe could delete real telemetry rows from a database that was over max_size_mb only because of free pages or an untruncated WAL, and incremental_vacuum reclaimed one page per call. Delete decisions now use the size of in-use pages. After deletes, free pages are reclaimed in bounded chunks and the WAL is truncated, so the file on disk can sit above the limit by its free pages plus the WAL. The cache's clear() runs a full VACUUM. (#2751)
  • Retention and size-failsafe cleanup resume when DatabaseService restarts instead of waiting out a full interval, and maintenance tasks configured shorter than the heartbeat run at their own interval. A retention cleanup that keeps skipping the cleanup of retired listeners and jobs escalates to ERROR after 3 consecutive cycles. (#2677, #2688, #2678)
  • Hassette shuts down cleanly, with the session finalized as a failure, when the web API can't bind its host and port. Before, the process ended with no teardown and the session row stayed running. (#2719)
  • A WebSocket disconnect no longer records a cancelled state_proxy_poll execution. (#2623)

Documentation

  • New guide for controlling Hassette apps from Home Assistant with the hass-hassette integration. (#2625)
  • Docker dependencies page explains that project install and app loading are separate import paths, and shows the src-layout build backend config and how to check the wheel with uv build --wheel. (#2683, #2691)
  • Troubleshooting page corrected: what the app precheck validates, the real class-not-found message, and how forgotten_await_behavior = "error" behaves. (#2745)
  • Bus methods page defines D, C and P, the accepted changed_from / changed_to forms and the shorthand methods' shared parameters. (#2721)
  • Blocking detection is described as event-loop stalls, CPU-bound work included, not only blocking I/O. The CLI reference explains cancelled and timed_out runs that make hassette job's Total exceed OK + Fail + Skipped. The lifecycle internals page states the correct ServiceWatcher shutdown order. (#2652, #2657, #2620)

0.56.0 (2026-10-08)

Breaking Changes

App code

  • AppConfig.app_key removed — the field was never populated, so self.app_config.app_key was always "". Read the app's key with self.app_key instead. An app_key left under an app's config in hassette.toml still loads, but is now an ignored extra field. (#2470)
  • ResourceStatus and ExecutionStatus import paths moved — hassette.types.ResourceStatus, hassette.types.enums.ResourceStatus and hassette.types.types.ExecutionStatus no longer resolve. Import them from the package root: from hassette import ResourceStatus, ExecutionStatus. ExecutionMode and BackpressurePolicy are also exported from the root. The LOG_LEVEL_TYPE alias is gone; use hassette_wire.LogLevel. (#2449)
  • AppConfig.log_level defaults to None — code that reads self.app_config.log_level directly now gets None for apps that don't set it, instead of "INFO". Use self.config_log_level for the resolved level. (#2571)

Web API, CLI JSON and hassette_wire

These affect scripts and dashboards that call the HTTP/WebSocket API, parse --json CLI output, or import hassette_wire models. App code is not affected.

  • Errors are RFC 9457 problem details — every non-2xx response under /api is now application/problem+json with type, title, status, detail and a stable code (see the new API errors page). Status codes and detail strings are unchanged. Validation 422s now carry a string detail summary instead of a list, and with the SPA served, a non-GET request to an unknown /api path returns 404 instead of 405. (#2480)
  • Telemetry outages return a problem body — data routes (telemetry, logs, executions, listeners, jobs, apps) now answer a database outage with a 503 telemetry_unavailable problem body instead of a 503 with an empty or default body. /api/health/ready and /api/telemetry/status are unchanged. (#2491)
  • Apps endpoints reshaped — update callers as follows (#2558):
  • GET /api/apps/manifests → GET /api/apps; the list field manifests → apps
  • GET /api/apps/{app_key}/manifest → GET /api/apps/{app_key}
  • The old GET /api/apps live-instance view (AppStatusResponse) is removed
  • GET /api/telemetry/dashboard/app-grid → GET /api/telemetry/app-grid, with rows shaped {app, activity} instead of flat
  • WebSocket event app_manifests_changed → apps_changed
  • hassette app --json and hassette dashboard --json rows are also {app, activity}; recent_invocations_1h leaves the app summary
  • Activity that wasn't computed is now null per part (stats, activity_buckets, last_error, blocking_event_count) instead of zeros — handle null
  • A non-finite since (nan, inf) now returns 422
  • One AppHealth model — GET /api/telemetry/app/{app_key}/health returns AppHealth, with averages renamed to handler_avg_duration_ms / job_avg_duration_ms (null when nothing of that kind ran). Grid rows nest error_rate, error_rate_class, health_status and last_activity_ts under health and drop avg_duration_ms. hassette app health --json renames handler_avg_duration / job_avg_duration to *_ms. Health now counts executions of handlers and jobs removed mid-window. (#2520)
  • Typed vocabularies — role, schedule_status, schedule_status_reason and app action fields are now enums in the OpenAPI and WebSocket schemas. ServiceInfo.role and JobSummary.schedule_status are now required. (#2527)
  • hassette_wire model renames — names now follow one rule (records are bare nouns, HTTP collections are *Response, WS payloads are *Data). Wire bytes are unchanged; only Python and schema component names change (#2584, #2558, #2520):
  • AppInstanceResponse → AppInstance, BootIssueResponse → BootIssue, ServiceInfoResponse → ServiceInfo, LogEntryResponse → LogEntry, AppSourceResponse → AppSource, ConnectedPayload → ConnectedData, ListenerWithSummary → ListenerSummary
  • AppManifestResponse → AppSummary, AppManifestListResponse → AppListResponse, DashboardAppGrid* → AppGrid*, ManifestStatus → AppStatus
  • AppHealthResponse → AppHealth; AppStatusResponse is removed

For App Authors: You Can Now Delete

  • await super().before_initialize() added only to keep self.cache working — the cache is now initialized by the framework before your hook runs, so an override no longer needs the super() call. (#2572)
  • cast() around self.cache.get(key, default=...) — get() with a non-None default is now typed as the default's type, so cast(list[float], ...) wrappers and or 0 fallbacks can go. (#2528)
  • Per-app log_level set only to match [hassette.logging] apps — apps without their own log_level now inherit the global apps level. (#2571)
  • deep_detection_enabled = true set alongside allow_deep_detection_in_prod = true — the allow flag now enables Tier 2 blocking-IO detection in production on its own. (#2472)
  • try/except inside predicates on duration= listeners to catch your own errors — a raising duration-hold predicate is now recorded as an error execution and routed to the listener's on_error handler, like any other predicate. (#2588)
  • Test code that rebuilds a simulated event to compare context IDs — AppTestHarness.simulate_*() helpers now return the event they dispatched. (#2593)

Python Client

  • hassette-client now ships HassetteClient: typed async methods over an aiohttp.ClientSession you own, returning hassette_wire models. Every network or server failure raises a HassetteClientError subclass (connection, timeout, HTTP status families, app-not-found, bootstrap-not-released), so callers branch on exception type instead of parsing response bodies. See the new Python client page. (#2596)
  • The server now reports api_schema_version in its health payload, and check_server_version() raises UnsupportedServerVersionError against a server too old for the client (#2596)
  • Client responses parse leniently: an enum or Literal value a newer server adds arrives as UnknownValue (a str subclass that keeps the raw value) instead of failing validation. Opt in on your own parses with hassette_wire.LENIENT_CONTEXT. (#2502)
  • hassette-wire now holds every model, enum and Literal served over the HTTP/WebSocket API, depending only on pydantic, so tools can use the API contract without installing the framework (#2449)

Configuration

  • New hassette.local.toml overlay for per-machine settings you don't want to commit (a dev base_url, a debug log level). Each configured TOML file gets its own <name>.local.toml sibling, which is deep-merged on top key by key, so it only needs the keys it changes. Env vars and CLI flags still win. Overlays are watched for hot reload. (#2561)
  • New per-app cache_shared = true silences the cache_key collision warning for apps that share a cache on purpose. The warning still fires, and names the missing apps, unless every app sharing the key sets it. (#2570)

Web UI & CLI

  • Blocking-IO findings are now visible: a "blocking calls" section on the app Overview tab, an "N blocking" badge on the Apps list, a "loop stalls" panel on Diagnostics for stalls not tied to an app, and a new hassette blocking command. Findings group by the app call site that blocked, with exact counts. Stalls recorded before upgrading show as "call site not captured". (#2495)
  • The Apps page and the hassette app / hassette dashboard tables show "—" for activity that wasn't computed, instead of zeros and a healthy-looking status. The CLI prints a one-line warning on stderr naming the missing parts. (#2558)
  • App health is computed the same way everywhere. The app detail health strip now reads it from the server and shows Handlers · Error Rate · Handler Avg · Job Avg; the CLI shows handler and job averages instead of one duration column. (#2520)
  • Config sections on the Config page and an app's Config tab can be collapsed and expanded (#2577)
  • An open dashboard tab prompts you to reload when the server is upgraded underneath it. The CLI keeps working against newer servers. (#2527)
  • CLI tables use the same widths and full-word headers across hassette app, hassette app activity and hassette status (App Key → App, Invoc/1h → Invocations/1h, Errs → Errors) (#2544)

Testing

  • AppTestHarness.simulate_*() helpers return the typed event they dispatched (for example RawStateChangeEvent from simulate_state_change()), after the triggered handlers have drained. Existing tests that ignore the return value are unaffected. (#2593)

Bug Fixes

  • WebSocket writes are no longer re-sent after a response timeout, which could apply a command twice. fire_event, call_service(return_response=True) without wait_for_ack, and helper create / update / delete now send exactly once and raise ResponseTimeoutError, or ResponseLostError on disconnect. Both subclass the new OutcomeUnknownError: if you relied on the old retry, catch it and check whether the command took effect before retrying. Reads still retry. (#2468)
  • Reloading or starting an app from the web API or CLI no longer takes the app down with HassetteNotInitializedError until a process restart (#2490)
  • Startup no longer times out when file_watcher.watch_files = false (#2501; also shipped in 0.55.1)
  • Apps without an explicit log_level now inherit [hassette.logging] apps instead of always using INFO (#2571)
  • self.cache works when an app overrides before_initialize without calling super(), instead of raising AsyncCache is not initialized (#2572)
  • allow_deep_detection_in_prod = true enables Tier 2 blocking-IO detection on its own. Note: an explicit deep_detection_enabled = true in production now turns Tier 2 on too; deep_detection_enabled = false always wins. (#2472)
  • A predicate that raises on a duration= listener is now recorded as an error execution and sent to on_error, instead of only being logged (#2588)
  • App keys containing /, \, an absolute path or a .. segment are rejected at config validation, so an app's derived cache directory can't escape data_dir or alias another app's (#2579)
  • WebSocket send and dispatch-failure logs and FailedMessageError messages no longer include payload values, which could contain secrets such as a password-mode input_text value. The payload is still on FailedMessageError.original_data. (#2565, #2567)
  • Shutdown logs carry the app's real key instead of app_key=None (#2470)
  • self.cache.get(key, default=x) is typed as x's type instead of T | None (#2528)
  • Invalid handler and where= predicate signatures (*args, positional-only parameters) no longer report a dependency injection error when the handler doesn't use DI (#2575)
  • Keyboard-focused execution table rows show a focus ring (#2583)
  • The OpenAPI and WebSocket schemas now include field descriptions for telemetry, blocking, health, WebSocket and auth models (#2538, #2541)

Documentation

  • New pages: API errors (every problem code) and Python client (#2480, #2596)
  • Accuracy pass across the core-concepts and testing pages: listener naming errors, state conversion rules, simulate_app_* signatures, enabled = false behavior, and config file discovery (every hassette.toml is merged) (#2465)
  • The bus methods page states correctly that omitting name= raises TypeError, and that every handler-taking method accepts on_error (#2441)
  • HelperClient.update() and delete() take the helper's storage id ("vacation_mode"), not its entity_id; the docs and docstrings now say so (#2450)
  • Blocking-IO guidance points to self.task_bucket.run_in_thread instead of asyncio.to_thread (#2523)
  • /api/health reports ok only once the WebSocket is connected and app bootstrap is released (#2471)
  • Fixed the A.get_path example path in the predicate reference (#2435)
  • The lifecycle internals page says who it's for and defines its terms (#2436)

0.55.1 (2026-10-05)

Bug Fixes

  • Startup no longer fails when file_watcher.watch_files is disabled. The file watcher service never marked itself ready on that path, so startup waited out the full timeout and then aborted. (#2501)

0.55.0 (2026-09-28)

Breaking Changes

  • TaskBucket.run_sync's timeout_seconds parameter now defaults to a NOT_PROVIDED sentinel instead of None. Previously, None meant "use the config default" and float("inf") meant "block forever" internally. Now, omitting the argument (or passing NOT_PROVIDED) means "use the config default," and None means "block forever" (matching concurrent.futures.Future.result(timeout=None)). run_sync is public, documented API — any caller that explicitly passed timeout_seconds=None to get the config default must omit the argument instead. (#2301)

Bus & API

  • New Bus.wait_for(topic, *, where=None, timeout, name=None) suspends until a matching event arrives and returns it, with no handler or subscription to manage. It only matches events dispatched after the call, which fits the "call a service, then wait for its effect" pattern. A sync-facade variant (self.sync.wait_for(...)) is also available. (#2301)
  • Topic is now exported from the top-level package: from hassette import Topic (#2360)
  • Api.call_service() gains wait_for_ack=True, which waits for Home Assistant to confirm the call and raises on failure. It works for services that return no response, where return_response=True would be rejected. (#2252)
  • Generated entity methods for services that return data now return the ServiceResponse instead of None: weather.get_forecasts(), todo.get_items(), media_player.browse_media(), and media_player.search_media() (#2237)

Telemetry & Database

  • New database.framework_retention_days (default 1) sets a separate, shorter retention window for framework-internal execution records. Previously they made up ~98% of the executions table and pushed app execution history out well before retention_days. Must be <= retention_days. After upgrading, the first cleanup removes framework records older than one day. (#2269)
  • Framework-tier executions are now recorded only when they are interesting. Errors and executions slower than 100 ms are always kept, and routine successes are dropped by default. Tune this with database.framework_record_errors, database.framework_record_slow_ms, and database.framework_record_sample_rate. Dropped records are counted in the new dropped_filtered field on /api/telemetry/status. App-tier executions are never filtered. (#2362)
  • The size failsafe now deletes framework executions first, then blocking events, then app executions, and it never deletes log records. Log expiry is controlled only by log_retention_days. (#2269, #2350)
  • Retention deletes now run in batches (database.retention_delete_batch, database.retention_max_batches_per_target), and a failure in one table no longer rolls back cleanup for the others (#2269)
  • New database.write_submit_timeout_seconds (default 60) bounds how long a telemetry write waits in the queue. A long retention pass now delays live telemetry instead of stalling it until the queue overflows. (#2377)
  • The database.heartbeat_interval_seconds, retention_interval_seconds, size_failsafe_*, and max_consecutive_heartbeat_failures settings are now honored. Previously they were accepted but ignored in favor of hardcoded values. (#2296)

Logging

  • New logging.extra_loggers setting sends loggers outside the hassette.* namespace (for example, your app's own my_app.notify logger or a third-party library) through Hassette's formatting, log capture, and database persistence (#1949)
  • Connecting to, reconnecting to, and disconnecting from Home Assistant are now logged at INFO with the URL, so a working token is visible at the default log level (#1959)
  • The effective log_persistence_level is now clamped to log_level, so the dashboard never hides records that are actively being logged (#2411)
  • web_api.log_buffer_size has been removed. Existing configs that set it still load. (#2411)

Web UI

  • The live log view now refreshes from the database instead of assembling entries from the WebSocket stream, so live rows carry the same execution/handler links as the initial page load. The live view shows the most recent 1000 records. (#2411)
  • The sidebar shows the app key instead of the display name when two apps share a display name (#2243)

CLI

  • A 401 from hassette status and other commands now says which credential was sent and where it came from, and lists the remedies (--token-file, cli.token_file, HASSETTE__CLI__AUTH_TOKEN). This also covers the confusing case where a second local instance's token is rejected. (#1960)
  • hassette job shows a Skipped column, so a predicate-gated job whose predicate never passes no longer reads as Total 68 / OK 0 / Fail 0 (#1958)

Packaging

  • hassette now depends on two new packages released in lockstep with it, hassette-wire and hassette-client. Both are currently empty placeholders for an upcoming split of framework internals. No imports change. (#2422)
  • The Docker image ships httpx2/httpcore2 2.12.0, clearing five published CVEs (#2151)

Bug Fixes

  • App failures no longer reach framework supervision. A failing app no longer produces spurious ServiceWatcher skipping restart warnings or risks a whole-process shutdown, and a crashing app no longer marks the session as failed. (#1955, #2324)
  • Starting an app that is already running no longer replaces its live instances and orphans their listeners, jobs, and tasks (#2245)
  • An instance left running after its app's configured instance count shrinks can now be stopped from the API and the web UI (#1962)
  • POST /api/apps/{app_key}/start and /reload now return 500 with the error message when the instance ends up FAILED, instead of reporting success with 202 (#2370)
  • HelperClient.increment(), decrement(), and reset() for counters no longer fail every time with service_validation_error (#2252)
  • Queued invocations dropped during teardown no longer permanently shrink a listener's available dispatch concurrency (#1957)
  • Reading state while the state cache is unavailable (for example, in an on_shutdown hook) now raises immediately instead of blocking the event loop for ~0.3–0.4 s in a retry that could never succeed (#1956)
  • Hot-reloading apps no longer accumulates duplicate logging filters, which caused slowly growing memory use and slower logging (#2247)
  • A stalled database write worker is now detected and restarted instead of leaving the service reporting healthy while telemetry silently stops (#1953)
  • Retention and size-failsafe cleanups that could not be queued are retried on the next heartbeat instead of being skipped for a full interval (#2351)
  • Sessions that recorded a crash but never shut down cleanly are now closed out on the next start, so their once=True listeners get cleaned up (#2246)
  • cleanup(timeout=0) on apps and resources now means "don't wait" instead of silently using the default shutdown timeout. Comparing a job that was never scheduled no longer raises AttributeError. (#2251)
  • predicate_description in listener and job telemetry (REST API, CLI --json, UI) shows a readable qualified name for bare-callable predicates instead of a bound-method repr with a memory address (#1961)
  • The Docker entrypoint no longer leaves a copy of the mounted project in /tmp/project-build.* on every container start (#2347)
  • Fixed missing web UI styles: secondary and muted text rendered at the wrong size and color, and dialog, drawer, popover, and tooltip animations were not running (#2379)
  • The Code tab's line-number gutter no longer shows an extra, empty final line (#2275)
  • Screen readers no longer announce the decorative icon on empty-state views (#2202)

Documentation

  • The first-automation guide now explains async handlers, placeholder entities, and the difference between handlers and scheduled jobs (#2412)
  • The testing docs define their harness terms, use valid MotionLights configs throughout, consolidate the seed-vs-simulate guidance into one place, and document that freeze_time takes a process-global lock (#2407, #2415)
  • Fixed the bus filtering "Custom Accessors" example, which type-checked but never matched at runtime (#2374)
  • Every predicate's summarize() now documents what it matches and the shape of the summary it returns (#2404)

0.54.0 (2026-09-05)

Breaking Changes

  • Vacuum battery attributes removed — VacuumEntityFeature.BATTERY, VacuumAttributes.battery_icon, VacuumAttributes.battery_level, and VacuumAttributes.supports_battery have been removed, mirroring their removal from Home Assistant core's base vacuum entity in 2026.9.0. Code reading vacuum battery state directly from these fields must switch to the entity's own battery sensor (e.g. a paired sensor.*_battery entity), which is how HA core itself now exposes this data. (#1908)

Typed Models

  • Regenerated typed entity/state models for Home Assistant 2026.9.0: open_cover/close_cover/set_cover_position gain an optional speed kwarg, and WaterHeaterEntityStateAttribute gains TARGET_TEMPERATURE as an alias of TEMPERATURE (#1908)

Bug Fixes

  • The dashboard's app status now updates live over WebSocket after an app load or reload, instead of only refreshing on an unrelated event. Fixes stale per-instance status on the multi-instance overview. (#1899)
  • Fixed a leaked database connection (and the non-daemon worker thread pinning it open) when a cache-shutdown task was cancelled mid-close (#1903)
  • Fixed the Handlers tab's mobile/desktop layout getting stuck in desktop mode when handlers or jobs loaded in after the initial render (#1886)

0.53.0 (2026-09-04)

Breaking Changes

  • hassette.test_utils no longer exists. Public (Tier 1) symbols move to hassette.testing; everything else moves to tests.support (internal-only, not shipped in the wheel — not usable outside this repo). (#1881) #### Public API — update the import path only
    • ApiCall, AppConfigurationError, AppTestHarness, DrainError, DrainFailure, DrainTimeout, RecordingApi, create_call_service_event, create_state_change_event, dummy_cache, make_light_state_dict, make_sensor_state_dict, make_state_dict, make_switch_state_dict, make_test_config, make_typed_state now import from hassette.testing instead of hassette.test_utils.

    Public API — newly promoted to Tier 1 (previously internal-only)

    • EventCapture, HassetteHarness, build_harness, make_full_state_change_event, wait_for are now part of the supported hassette.testing surface.

    Removed from the public surface

    • make_mock_hassette was previously in hassette.test_utils.__all__; it is now internal-only (tests.support.mock_hassette, not shipped in the wheel). External consumers using it should build their own Hassette stub, or open an issue if this needs to remain public.

    Internal-only, no longer importable at all outside this repo

    • Every symbol not listed above (web-layer factories, seed scenario helpers, SQL/uvicorn test servers, FakeStateReader, create_app_manifest, make_addrinfo, patch_loop_getaddrinfo, etc.) is no longer part of the wheel. It lived at hassette.test_utils.* before; it now lives at tests.support.* inside this repository only.
  • SensorAttributes no longer declares native_value, native_unit_of_measurement, or suggested_display_precision (they duplicated the base State.value/unit_of_measurement fields and are removed for the sensor domain only). StateRegistry.register and register_state_converter drop their unused device_class keyword parameter, and StateKey drops its device_class field (kept as a one-field frozen dataclass with domain only — its export names and isinstance checks are unchanged). (#1549) #### What changes for callers
    • Code reading sensor_state.attributes.native_value, .native_unit_of_measurement, or .suggested_display_precision must switch to sensor_state.value and sensor_state.attributes.unit_of_measurement — those fields already carried the same data.
    • Code calling StateRegistry.register(..., device_class=...), StateRegistry.resolve(..., device_class=...), or register_state_converter(..., device_class=...) must drop the device_class keyword — it was never populated or read by any production call site.
    • Code constructing StateKey(domain=..., device_class=...) directly must drop device_class — StateKey is now StateKey(domain) only.

Features

  • New device-class-specific sensor state subtypes, so a sensor's .attributes are typed to that device class instead of the generic sensor shape (#1549)
  • Per-instance app restart — restart a single instance instead of every instance under an app key, from Python, the REST API, the CLI, and the dashboard (#1687, #1873)
  • Shutdown-phase timing is now logged (wave-based child shutdown, task-bucket cancellation, cleanup(), child-shutdown propagation), so a shutdown timeout log shows which phase consumed the budget instead of just "shutdown timed out" (#1766)

Bug Fixes

  • Fixed a bundle of small issues: incorrect fire_at display for non-jittered jobs, a wrong app_activity default time window, unhelpful CLI error framing for malformed 2xx responses, duplicate app-init error logs, log-table key collisions on seq: 0, and query-param parsing edge cases (#1872)
  • Job/handler status reporting is now exhaustive over all statuses, fixing gaps where degraded/skipped states weren't handled (#1671)
  • hassette run now handles SIGINT with a graceful shutdown instead of hanging silently (#1863)
  • Non-active handler health cards no longer sit in the keyboard tab order (#1742)
  • A resource whose teardown couldn't be confirmed no longer triggers a restart on top of it (#1723)
  • Warnings captured via Python's warnings module now flow through the logging pipeline instead of going to stderr only (#1864)
  • Hardened shutdown/restart: service-watcher event dispatch is now bounded by a timeout, cancellation causes are distinguished during resource cleanup, and terminal-status evidence survives a forced termination (#1802)
  • Widened several typed state attributes that are declared as strict int/enum but that real HA platforms sometimes populate with out-of-range or string values, which previously raised validation errors (#1775)

Performance Improvements

  • The dashboard no longer broadcasts every state change over WebSocket to browser clients — cut a source of unnecessary traffic and re-renders (#1639)
  • App-detail pages (and the components mounted inside them) now scope their store subscriptions to their own app, instead of re-rendering on every other app's status and execution activity fleet-wide (#1528, #1607)

Documentation

  • Fixed stale accuracy claims in lifecycle.md (#1760)
  • Replaced a stale note in the caching docs that pointed at # pyright: ignore comments that don't exist in the referenced snippet (#1518)

0.52.0 (2026-08-07)

⚠ BREAKING CHANGES

  • Web API requires authentication by default — every /api/* route (except GET /api/health/live, GET /api/health/ready, and POST /api/auth/session) returns 401 without a valid bearer token, session cookie, or trusted_proxies match. #### What changes for operators
    • On first start after upgrading, hassette generates a token, logs it once, and persists it to <data_dir>/.web_api_token. Retrieve it from docker logs (or the token file) and paste it into the new login view, or set HASSETTE__WEB_API__AUTH_TOKEN explicitly.
    • CLI commands (hassette status, hassette app, etc.) now attach the token automatically from the token file or HASSETTE__WEB_API__AUTH_TOKEN — no action needed if running the CLI on the same host/volume as the server.
    • Operators fronting hassette with a forward-auth gateway (Authelia, tinyauth+pocket-id, the HA add-on's ingress) should set trusted_proxies to the gateway's address instead of managing a token.
    • cors_origins may no longer contain "*" — config load now rejects it, since allow_credentials=True is hardcoded.
    • GET /api/docs and /api/openapi.json now require a credential like every other /api/* route.
    • Setting auth_enabled=false requires host to be a loopback address (127.0.0.1/localhost) — hassette refuses to start otherwise. (#1521)
  • CLI no longer forwards local credentials to a remote target — the CLI no longer sends the local instance's own credentials (web_api.auth_token / HASSETTE__WEB_API__AUTH_TOKEN, or the <data_dir>/.web_api_token file) to a non-loopback target. Previously, pointing the CLI at a remote host via HASSETTE__WEB_API__HOST=<host> silently transmitted the local instance's own credential to that host. A script or profile relying on this now either fails with a 401 or, against a target with trusted_proxies configured for it, keeps working with no credential sent at all. Configure a remote-target credential via --token-file, cli.token_file, or HASSETTE__CLI__AUTH_TOKEN instead. Separately, hassette run's --base-url and --url flags are removed — use --ha-url (same -u short flag). (#1529)

Web API

  • Session cookie auth for the dashboard, pre-accept WebSocket checks — browsers exchange the token once via POST /api/auth/session for an HttpOnly session cookie, since the native WebSocket API can't set custom headers. The WebSocket handler now runs its credential check before accept(), so no application data reaches an unauthenticated peer. (#1521)
  • Failed authentication attempts are logged — rate-limited and coalesced into a single WARN per source once attempts cross a threshold, instead of going unnoticed. (#1521)

CLI

  • --server-url, --token-file, and --no-verify-ssl flags — administer a remote Hassette instance behind a reverse proxy with TLS. A new [hassette.cli] config group (server_url, verify_ssl, token_file, auth_token) holds connect settings separately from web_api.*'s bind settings. Error messages for a 401 or a redirect now name the resolved target and the fix. (#1529)

Security Fixes

  • Request bodies are now size-limited before authentication — an unauthenticated peer could previously drive unbounded memory allocation against POST /api/auth/session by sending an oversized body; request bodies are now capped at 64 KiB and rejected with 413 before the route runs. (#1532)
  • A wrong or malformed bearer token from a trusted proxy is now rejected — when a request arrived from a trusted_proxies peer, hassette previously never checked the token at all, silently authenticating the request even with a wrong token. A presented Authorization header is now always validated; peer trust and the session cookie apply only when no header is sent. (#1531)
  • Secrets nested inside lists, tuples, and dicts are now masked — a SecretStr value inside a container field (list[SecretStr], dict[str, SecretStr], a SecretStr inside a nested model) was previously returned in plaintext by GET /api/config and the per-app config endpoint. (#1534)
  • Codegen renders external strings as safe literals — Home Assistant constants and services.yaml descriptions containing quotes or """ could previously produce broken or semantically wrong generated Python; codegen output now escapes through dedicated rendering helpers instead of raw string interpolation. (#1535)

0.51.0 (2026-08-01)

⚠ BREAKING CHANGES

  • Scheduler renames: ScheduledJob → Job, cancel → remove — ScheduledJob is renamed to Job, the single public runtime type returned by both schedule() and the new register(). ScheduledJob.cancel() → Job.remove(), Scheduler.cancel_job() → Scheduler.remove_job(), Scheduler.cancel_group() → Scheduler.remove_group(). The persisted cancelled_at columns are renamed to removed_at (migration 012 handles this automatically). next_run == null no longer implies a job has finished — read the new schedule_status field (scheduled, waiting, completed, or manual) instead. No compatibility aliases are kept. (#1500)
  • Health endpoint reports degraded during app bootstrap — GET /api/health/ready now returns 503 during the window between WebSocket connection and initial state-sync completion, where it previously returned 200. SystemStatusResponse gained a bootstrap_released field, and a synthesized boot issue ("Apps pending on Home Assistant") appears while bootstrap is pending. Monitoring integrations polling /api/health/ready will see the new 503 window on upgrade. (#1495)
  • SystemStatusResponse.log_records_dropped split into two fields — replaced by log_queue_drops and db_write_queue_drops. Consumers reading the old field should sum the two new ones, or read whichever queue they care about. Hassette.get_log_records_dropped() is likewise replaced by get_log_queue_drops() and get_db_write_queue_drops(). (#1469)
  • ListenerWithSummary.entity_id renamed to target — the web API response field (on GET /api/bus/listeners and GET /api/telemetry/app/{key}/listeners) now populates event-type listeners with a topic-derived name instead of null. (#1442)
  • only_app renamed to only_apps — the field on GET /api/apps and GET /api/apps/manifests now returns a list of app keys instead of a single key or null. (#1464)

Scheduler

  • Manual-only job registration — Scheduler.register() creates a named job with no automatic trigger, addressable via Job.submit() from Python, the REST API, the CLI, or the UI's Run Now button. No fake far-future schedule required. (#1500)
  • Explicit schedule status — jobs carry a schedule_status field (scheduled, waiting, completed, or manual) instead of inferring state from a null next_run. One-shot jobs stay visible as completed after firing instead of disappearing. (#1500)
  • EntityTime trigger — schedule a job driven by an entity's time-valued state (e.g., sun.sun's next_rising). The job re-evaluates whenever the entity updates and stays in waiting when the source entity is temporarily unavailable. (#1471)

API

  • Api.notify() and Api.get_notify_services() — typed wrapper for HA's notify services with notifier normalization (accepts "mobile_app_phone" or "notify.mobile_app_phone"), available through the sync facade and test harness (#1422)
  • State and type registries exposed on App instances — self.state_registry and self.type_registry give app authors direct access to the conversion registries without reaching into framework internals (#1481)
  • hassette run --app — run only named apps instead of the full config (#1425)
  • Configurable timeout on outbound REST requests — all REST calls to Home Assistant now have an explicit configurable timeout instead of waiting indefinitely (#1497)

Dashboard

  • New color palette and app shell redesign — higher-chroma palette with distinct job/listener family colors; health indicators and theme toggle moved to a collapsible sidebar footer; status bar shows route-derived breadcrumbs; sidebar collapses on desktop (persisted, toggled with [) (#1414)
  • Frontend migrated to React 19, shadcn/ui, and Tailwind v4 — full migration from Preact/CSS Modules. No backend behavior changes. (#1478)
  • Dashboard starts without a live HA connection — the web UI is reachable while Hassette waits for Home Assistant, showing starting status and queryable metadata; apps bootstrap only after initial state sync completes (#1458)
  • Toast feedback for app actions — start, stop, and reload now show success/error toasts instead of silently succeeding (#1424)
  • Apps table shows display names instead of raw app_key (#1504)
  • Docker images tagged with semver — releases now publish v0.51.0, v0.51, and v0 tags alongside latest (#1475)

Health & Observability

  • Log persistence health reporting — GET /api/health now reports whether log writes to the database are succeeding, alongside per-queue drop counts (log_queue_drops and db_write_queue_drops) (#1468, #1469)
  • Saturation/capacity warning thresholds configurable — bus and scheduler queue saturation thresholds are now config fields instead of hard-coded constants (#1498)
  • App manifests persisted to the database — the apps list and dashboard read from persisted manifests instead of runtime-only state, so registered apps remain queryable across restarts (#1464)

Bug Fixes

  • fix state model cache keying on context ID instead of last_updated, causing stale cached models to persist across state changes — thanks @thiesgerken! (#1496)
  • wait for initial WebSocket connection before attempting state sync, preventing a race where state sync could start before authentication completed (#1483)
  • rate-limit empty app_key warning on execution completion events to one per minute instead of per-event (#1480)
  • fix page title sizing and baseline rule for consistent heading hierarchy (#1439)
  • fix heading+badge row alignment with mixed font sizes (#1459)
  • fix filter panel layout shift when opening on pages with long content (#1423)

0.50.0 (2026-07-24)

⚠ BREAKING CHANGES

  • Resource.cache replaced by instance-scoped App.cache — cache access moves to App only. The API is now async (await self.cache.get(key), await self.cache.set(key, value, ttl=...), await self.cache.get_or_set(key, creator, ttl=...), .clear(), .invalidate(*keys)), with a self.cache.sync accessor for sync equivalents. HassetteConfig.default_cache_size is replaced by HassetteConfig.default_cache_ttl (seconds). diskcache is no longer a dependency, and existing on-disk cache data is orphaned on upgrade. (#1361)
  • Helper CRUD methods namespaced under Api.helpers — all 35 flat helper methods (list_input_booleans(), create_input_boolean(), update_counter(), delete_timer(), increment_counter(), etc.) are removed from Api, ApiSyncFacade, RecordingApi, and RecordingApiSyncFacade. Use api.helpers.list("input_boolean"), api.helpers.create(CreateInputBooleanParams(...)), api.helpers.update(helper_id, UpdateCounterParams(...)), api.helpers.delete("timer", helper_id). The sync facade mirrors this at api.sync.helpers.*. (#1362)

Features

  • replace Resource.cache with instance-scoped async App.cache — per-app cache with TTL support, WAL-mode SQLite, and corruption recovery (#1361)
  • render raw config as TOML with syntax highlighting on the config tab (#1405)
  • ui: add execution detail page with linked execution IDs (#1375)
  • ui: refine app detail handler diagnostics (#1411)

Bug Fixes

  • key appStatus WebSocket state per app instance — app detail pages now show status for the correct instance (#1399)
  • move tomli-w to production dependencies to fix Docker crash (#1409)
  • replace clickable log table rows with explicit detail controls (#1403)
  • thread time-window into log fetch and deduplicate by row key (#1397)
  • ui: fix column picker popover clipping at viewport bottom (#1406)

Refactoring

  • namespace helper CRUD behind Api.helpers — replaces 35 flat methods with 4 generic overloaded methods plus counter shortcuts (#1362)

0.49.0 (2026-07-16)

⚠ BREAKING CHANGES

  • Registration signatures frozen — name is now a required keyword-only argument on every Bus and Scheduler registration method (on_state_change, on_attribute_change, on_call_service, on, on_homeassistant_*, on_websocket_*, on_app_*, on_hassette_service_*, schedule, run_in, run_once, run_every, run_minutely, run_hourly, run_daily, run_cron) and their sync facades. Omitting name raises TypeError instead of the old ListenerNameRequiredError — Pyright catches this at development time. All optional parameters must be passed by keyword. Update call sites to pass name="..." explicitly and convert any positional group=/jitter=/timeout=/timeout_disabled=/if_past= arguments to keyword form. (#1301)
  • DomainStates keys/values are lazy iterators — DomainStates.keys() and .values() now return iterators instead of lists, matching items() and __iter__. Code that calls len(), indexes, or iterates multiple times must wrap the result in list(). iterkeys() and itervalues() are removed. (#1325)
  • Entity type aliases renamed with domain prefixes — Format → CameraFormat, Direction → FanDirection, Flash → LightFlash, Enqueue → MediaPlayerEnqueue, Repeat → MediaPlayerRepeat, CommandType → RemoteCommandType, Status → TodoStatus, Type → WeatherType. Update imports to the new names. (#1306)
  • Event factory classmethods renamed — HassetteServiceEvent.from_data() → from_service_status(), HassetteAppStateEvent.from_data() → from_app(), HassetteSimpleEvent.create_event() → from_topic(), HassetteFileWatcherEvent.create_event() → from_paths(). Update any code constructing these events directly. (#1306)
  • ResourceRole string values lowercased — "Core", "Service", "App", "Base", "Resource", "Unknown" changed to "core", "service", "app", "base", "resource", "unknown". Code comparing role == "Core" must switch to lowercase. Enum member names are unchanged. (#1306)
  • Api.get_state_value_typed() removed — also removed from ApiSyncFacade, RecordingApi, and RecordingApiSyncFacade. Use get_state().value for a typed value or get_state_value() for the raw string. (#1306)
  • Api.toggle_service renamed to toggle — also renamed on ApiSyncFacade, RecordingApi, and RecordingApiSyncFacade. Update calls from api.toggle_service(...) to api.toggle(...). (#1321)
  • Timezone utility functions renamed — convert_datetime_str_to_system_tz → convert_datetime_str_to_tz, convert_utc_timestamp_to_system_tz → convert_utc_timestamp_to_tz. Behavior unchanged when HassetteConfig.timezone is unset. (#1327)
  • Pre-v1 backwards compatibility shims removed — deprecated aliases and transitional APIs are gone. (#1308)

Features

  • add configurable timezone for wall-clock scheduling — Daily and Cron triggers respect HassetteConfig.timezone, defaulting to the system timezone (#1327)
  • auto-derive service domain from entity ID and return typed results from entity methods — the manual domain parameter is no longer needed on call_service() (#1321)

Bug Fixes

  • cli: fix --since flag crash on all CLI commands (#1290)
  • preserve telemetry data during schema migrations instead of dropping it (#1298)

Refactoring

  • App's public API surface reduced — lifecycle machinery extracted to module-level functions; App exposes ~20 names in autocomplete instead of ~54, hiding framework internals from app authors (#1314)

0.48.0 (2026-07-09)

⚠ BREAKING CHANGES

  • remove hassette event CLI and events API endpoint (#1192)

Features

Bug Fixes

  • entity.turn_on()/turn_off()/toggle() now use the homeassistant domain (#1205) (9dbb403), closes #1021
  • bus predicates that raise no longer silently swallow the error (#1257) (9d655d2), closes #1243 #1244
  • sync handler executor no longer crashes on rapid restart (#1263) (1533a56), closes #1225 #1226
  • app listeners and jobs are cleaned up on reload instead of leaking (#1264) (1269eb8), closes #1220 #1221
  • mask app secret fields in global GET /config manifest data (#1171) (65966b7), closes #1139
  • generated state model enum names now use canonical Python conventions (#1201) (7f507c7), closes #1199
  • contextvars now propagate to sync handler worker threads (#1172) (c414353), closes #1145

Performance Improvements

  • reduce re-renders on the logs page under live WebSocket updates (#1254) (d5b3fd3)

Documentation

0.47.0 (2026-07-02)

Breaking Changes

  • Secret masking is now type-driven — HassetteConfig.token is now SecretStr | None instead of str | None. Code reading the token must unwrap it with token.get_secret_value(). App-config secret masking follows the declared type: a field is masked only if it is typed SecretStr. The old name-based regex (token/password/secret/api_key) is removed. App authors who relied on name-based masking must type those fields SecretStr to keep them hidden in the dashboard. (#1131)

Config UI

  • Unified config view with complete field coverage — the global Config page and per-app Config tab now share a single backend and renderer. Previously, the global page hand-maintained a mapper that silently dropped whole config groups (database, websocket, blocking_io); all fields are now visible. Secrets are masked by type (SecretStr), values format by type (booleans as badges, durations humanized, paths as code), and app authors can add presentation hints via json_schema_extra ui metadata on their AppConfig fields. (#1131)
  • Scannable field list layout — the config view is redesigned from a 3-column table into a field list with distinct visual registers: human label, TOML key, and formatted value. Field descriptions move into a click-to-open info popover. Fixes a 360px horizontal overflow on the old table layout. (#1151)
  • User-defined and framework fields separated — the per-app Config tab now partitions fields into "App Settings" (user-defined) and "Hassette Settings" (framework-managed like log_level, blocking_io_behavior). enabled and autostart now appear in the config tab. (#1154)

Bug Fixes

  • hassette app config shows values instead of schema blob — the CLI command was dumping the raw inlined JSON schema into the terminal instead of the masked config values (a regression from the config unification). (#1138)

0.46.0 (2026-06-25)

Breaking Changes

  • State/type registries moved to hassette.conversion — STATE_REGISTRY and TYPE_REGISTRY are now owned by hassette.conversion instead of hassette.models. Imports from hassette.models still work via re-exports but are deprecated; update to from hassette.conversion import STATE_REGISTRY, TYPE_REGISTRY. (#1122)

Apps

  • autostart config option — register an app without starting it at boot. enabled = true, autostart = false registers the app, shows it in the dashboard, and leaves it startable on demand, with enabled still the hard on/off switch. These apps get a "no autostart" chip in the UI and a new Autostart column in hassette app. (#1124)

Bus

  • Per-listener backpressure policy — each subscription chooses block (default; wait for a dispatch slot, unchanged behavior) or drop_newest (skip the event when the bus is saturated). Drop counts surface in the listener detail view. (#1082)
  • Bounded concurrent dispatch — new lifecycle.max_concurrent_dispatches config field (default 50) caps how many handler invocations run at once, so a Home Assistant state storm can no longer fan out into unbounded tasks and exhaust memory. (#1075)

State Models

  • Presence helpers on StateManager — anybody_home(), everybody_home(), nobody_home(), and is_home(entity_id), accessed via self.states. Each reads the local state cache synchronously (no await, no network round-trip) and checks the person domain, falling back to device_tracker when no person entities exist. (#1130)
  • Domain-typed state access (self.states.light["x"] / .get("x")) now raises UnableToConvertStateError on a conversion failure instead of leaking a raw pydantic.ValidationError. Code catching ValidationError from domain-typed access should catch UnableToConvertStateError instead. (#1109)

Bug Fixes

  • Manual app reload picks up fixed code — the REST/UI Reload action now forces a fresh module re-import, so an app that failed to start no longer keeps failing with the original error after you fix the source on disk. Matches the file-watcher's behavior. (#1110)
  • config_log_all_events respects hot-reload — it was frozen after the first dispatched event and silently ignored config changes; it now updates live like config_log_level. (#1088)
  • Cold-start state retry now fires — StateProxy.yield_domain_states was retrying a generator object rather than the readiness check, so the @retry never re-ran before the state cache was ready. (#1088)
  • Wiring guards survive python -O — service-wiring assert statements (stripped in optimized Docker images) are now real runtime checks. (#1088)
  • Per-section app manifests no longer clobber each other — two [apps.*] sections referencing the same App subclass kept distinct display_name/enabled/auto_loaded; the manifest is now per-instance instead of a shared class attribute. (#1069)
  • Resources that already reached a terminal state are no longer driven to FAILED during teardown — a late submit-after-shutdown on an already-STOPPED resource is a benign no-op, silencing a spurious strict-mode warning. (#1071)

0.45.0 (2026-06-16)

Monitoring UI

  • Cancelled executions are now surfaced across the monitoring UI — handlers and jobs dropped, restarted, or queued out by overlap modes show a cancelled count and status in the summary tables and detail panes. (#1057)
  • Job and handler summaries now show a thread_leaked count, surfacing sync work whose worker thread leaked (e.g. a timed-out handler that never returned its thread to the pool). (#1052)
  • Execution-scoped log views now include framework logs alongside app logs, so a single execution's full log context is visible in one place. (#1051)

Bug Fixes

  • Two config sections that share the same App class now resolve to distinct app_keys instead of colliding, so telemetry and lookups no longer attribute both instances to one key. (#1064)
  • Blocking-I/O detection now attributes the offending app correctly under load; a busy event loop previously could blame the wrong app for blocking calls. (#1054)

0.44.0 (2026-06-16)

Breaking Changes

  • App-tier bus handlers now default to single execution mode — a trigger that fires while a prior invocation of the same handler is still running is dropped instead of running concurrently. Apps that rely on overlapping invocations must pass mode="parallel" on the registration (e.g. self.bus.on_state_change(..., mode="parallel")). Framework-tier listeners are unchanged and still default to parallel. (#1028)

Execution Overlap Modes

  • Bus handlers gain per-listener overlap modes — single (drop the re-fire), restart (cancel and replace), queued (serialize), and parallel (run concurrently) — controlling what happens when a handler is re-triggered while still running. App-tier defaults to single; framework-tier to parallel. (#1028)
  • Scheduled jobs gain the same overlap modes via a mode= parameter on run_in, run_once, run_every, run_daily, and run_cron. Framework jobs default to parallel, app jobs to single. (#1046)

Event Loop Protection

  • Sync handlers now run on a dedicated, isolated thread pool, so a slow or timed-out handler can no longer starve framework internals like logging and the database. Tune the pool with the new sync_executor_max_workers config. (#1042)
  • Blocking I/O run directly on the shared event loop (e.g. time.sleep, requests.get, blocking open().read()) is now detected, attributed to the offending app, and logged. Control it with blocking_io_behavior globally or per-app, or set it to "ignore" to disable. (#1040)

0.43.0 (2026-06-13)

Breaking Changes

  • Duplicate once=True listeners now raise DuplicateListenerError — two once-listeners with the same name and topic no longer silently coexist. Use distinct names, or the new if_exists parameter, to register more than one under a shared name+topic. (#1015)
  • Bus.add_listener now returns a Subscription instead of None — callers that asserted the return was None must update; callers that ignore the return are unaffected.

Features

  • Typed sync facades for all domain entity classes, giving synchronous code the same typed entity access as async handlers. (#1020)
  • if_exists parameter on bus handler registration — "error" (default) or "skip" for idempotent registration. (#1015)
  • Forgetting to await a registration call (bus.on_state_change, scheduler.run_in, api.call_service) now raises a runtime warning naming the exact call site, plus a static Pyright signal — instead of failing silently. (#1019)

Bug Fixes

  • CLI: telemetry status now shows a degraded status on HTTP 503 instead of exiting with an error. (#1016)

0.42.0 (2026-06-11)

Features

  • Web UI: redesigned diagnostics page and fixed table cropping on mobile. (#1013)

Bug Fixes

  • Comparison condition coerces numeric strings, so comparisons against string-typed states behave as expected. (#1003)
  • Docker semver image tags are published again — they had been missing since v0.39.0. (#999)

Documentation

  • Full documentation site rewrite — getting started, core concepts, web UI, CLI, testing, recipes, migration, and troubleshooting, with tested code snippets throughout. (#970)

0.41.0 (2026-06-08)

Features

  • The state cache retains last-known values on disconnect instead of clearing them, so apps keep reading the last known state through a brief HA outage. (#996)

Bug Fixes

  • Log timestamps now use local time instead of UTC, matching the host timezone; send_and_wait retries transient WebSocket transport timeouts with backoff; and the state proxy's periodic poll survives disconnect to self-heal if reconnect resync fails. (#990)
  • State subscription is re-established on reconnect even when the cache reload fails, with a concurrency guard that serializes overlapping reconnects when the WebSocket flaps. (#995)

0.40.0 (2026-06-06)

Breaking Changes

  • GET /api/health now returns 200 (not 503) whenever the process is serving — for starting and degraded as well as ok; it never returns 503. Move restart automation to /api/health/live (liveness, independent of HA) and traffic routing to /api/health/ready (200 only when fully connected). Separately, a fatal unrecoverable shutdown now exits non-zero, so supervisors using Restart=on-failure restart after a crash; a clean operator shutdown (SIGTERM / docker stop) still exits 0. (#978)

Features

  • Split health checks into /api/health/live (liveness) and /api/health/ready (readiness) endpoints, fixing the reboot loop where an HA outage made an external healthcheck restart Hassette. Fatal crashes now exit non-zero so supervisors can act on them. (#978)

0.39.1 (2026-06-05)

Bug Fixes

  • health endpoint returns 200 for degraded status, 503 only for starting (#975) (d09b709)

0.39.0 (2026-06-01)

⚠ BREAKING CHANGES

  • App.send_event(topic, data) and AppSync.send_event_sync(topic, data) are removed. Use self.bus.emit(topic, data) instead — same arguments, same behavior, now on the bus where subscriptions live.
  • HassettePayload no longer accepts an event_type parameter. Remove event_type= from any HassettePayload(...) or HassetteServiceEvent(...) construction. EventPayload base class also no longer has event_type — use HassPayload.event_type for HA events, or event.topic for routing.

Features

  • broadcast custom events between apps with Bus.emit() and receive typed payloads via D.EventData[T] (#952) (082a59e)

Bug Fixes

  • prevent aiosqlite worker threads from blocking interpreter exit (#923) (#948) (a17b57b)

Refactoring

  • remove event_type from EventPayload base and HassettePayload (#947) (8037297)
  • remove redundant event_name parameter from send_event (#946) (e282f81), closes #943

0.38.0 (2026-05-31)

Features

  • add Python 3.14 support (#939) (86049f2)
  • add missing synchronous Bus and Scheduler methods for AppSync (#931) (962631f)

0.37.0 (2026-05-30)

⚠ BREAKING CHANGES

This release redesigns registration and telemetry. Most existing apps need code changes.

Registration is now async

Bus and scheduler registration methods are now async and must be awaited.

name= is now required on every bus registration (it was optional). Omitting it raises ListenerNameRequiredError.

# before
self.bus.on_state_change("light.kitchen", handler=self.on_change)
self.scheduler.run_in(self.task, 5)

# after
await self.bus.on_state_change("light.kitchen", handler=self.on_change, name="kitchen_light")
await self.scheduler.run_in(self.task, 5)

Affected bus methods: on_state_change, on_attribute_change, on_call_service, on_component_loaded, on.

Affected scheduler methods: schedule, run_in, run_once, run_every, run_minutely, run_hourly, run_daily, run_cron.

Subscription.registration_task is removed. Registration completes inline now, so sub.listener.db_id is a valid integer as soon as the awaited call returns.

Unified executions model

HandlerInvocation and JobExecution are replaced by a single Execution model with a kind discriminator ("listener" or "job").

The REST endpoint /api/telemetry/handler/{id}/invocations is now /api/telemetry/listener/{id}/executions. The WebSocket signals are unified under the same executions shape.

Migration runner replaces Alembic

The Alembic migration stack is replaced by a PRAGMA user_version runner with plain SQL files. Existing databases are migrated automatically on first startup. No manual migration steps required, but any tooling that reads alembic_version will need updating.

Removed: dropped_no_session

The dropped_no_session counter is removed from the API response, the dashboard status badge, and the sessions table.

Features

  • redesign telemetry database around a unified executions table (#922) (b97a495)

Bug Fixes

  • fix starlette host header injection (CVE-2026-48710) (#907) (c9ad12f)

Internal

  • decompose bus_service.py into focused modules (#919)
  • extract Service and RestartSpec from resources/base.py (#921)
  • docs voice guide, quality rules, and audit (#910, #917, #920)

0.36.0 (2026-05-28)

Features

  • ui: apply design rules — typography, spacing, radii, tables, and visual cleanup (#905) (8183d57)

0.35.0 (2026-05-27)

⚠ BREAKING CHANGES

  • hassette with no arguments no longer starts the server. Use hassette run instead. The hassette service command and GET /api/services endpoint have been removed.

Features

  • require explicit hassette run to start the server (#896) (d8bbaa3)

0.34.0 (2026-05-27)

Breaking Changes

  • state._has_feature(SomeFeature.FLAG) renamed to state.has_feature() — remove the leading underscore. The supports_* convenience properties (e.g., state.supports_brightness) are unchanged. (#891)
  • Api.get_states_iterator() and ApiSyncFacade.get_states_iterator() removed — the generator-based iterator does not work correctly in sync code. Use get_states() instead. (#891)

Logging

  • Logging pipeline managed as a LoggingService Resource with lifecycle ordering — the async pipeline starts during service initialization and shuts down before the database, ensuring pending log records flush cleanly. (#887)
  • Timer-based flush bounds how long log records sit in the write queue under low load (default 5s), instead of waiting for the next batch to fill. (#890)
  • Failed write batches retry with linear backoff (1s, 2s, 3s) instead of immediately, giving the database time to recover. (#890)

CLI

  • Improved hassette status/hassette query output formatting — nested sub-models render as labeled sections instead of raw JSON, None shows as —, booleans show lowercase, and panel titles are humanized. (#880)
  • Job and listener tables show App column, actual schedule text, method-only handler names, and entity targets. (#880)
  • fmt_relative_time shows in 3h for future timestamps instead of soon. (#880)

Bug Fixes

  • Fix 3 bugs in example apps where BinarySensorState.value and LightState.value (both bool) were compared against string "on"/"off". (#872)

0.33.0 (2026-05-26)

Breaking Changes

  • GET /api/logs/by-execution/{execution_id} has been removed. Use GET /api/executions/{execution_id} instead. The response shape (LogsByExecutionResponse) is unchanged. (#865)

CLI

  • hassette query subcommand for querying all read-only API endpoints from the command line — apps, handlers, jobs, invocations, executions, logs, config, and sessions. (#838)
  • --generate-completion replaces --install-completion — outputs the shell completion script to stdout instead of modifying shell config files. Pipe to a file in your $fpath to install. (#870)
  • Fix zsh completion function name with cyclopts 4.16+ — hassette --generate-completion zsh now produces the correct _hassette function name instead of _cyclopts_hassette. (#871)

Web UI

  • Redesigned accent color system and improved visual depth with systematized design tokens. (#842)
  • Mobile navigation integrated into the status bar with a hamburger menu. (#857)
  • Unified table sort headers across all pages and fixed mobile table column rendering. (#853)
  • Aria-live announcements for WebSocket connection status changes. (#827)

Configuration

  • TOML [hassette] section now deep-merges, preserving top-level app definitions alongside nested sections. (#826)
  • Auto-migrate legacy HASSETTE__APP_DIR environment variable to HASSETTE__APPS__DIRECTORY. (#831)
  • Environment variables correctly override config file values for legacy flat config keys. (#836)

Performance

  • Telemetry database queries now have a configurable read timeout, preventing indefinite hangs on slow or locked databases. (#860)

Documentation

  • Rewrite web UI documentation with full page coverage. (#806)
  • Update configuration references to post-v0.32.0 nested key format. (#818)
  • Clean up docs site navigation — remove redundant tabs, fix mid-viewport layout. (#858)

0.32.0 (2026-05-20)

Breaking Changes

  • TOML app configuration path changed from [hassette.app.apps.<name>] to [hassette.apps.<name>]. The app settings section changed from [hassette.app] to [hassette.apps]. Environment variables changed from HASSETTE__APP__* to HASSETTE__APPS__*. Python access changed from config.app.* to config.apps.*. The API response field changed from app to apps. Update your hassette.toml, environment variables, and any code that accesses the config object.

Refactoring

  • flatten app config path from [hassette.app.apps] to [hassette.apps] (#792) (a4251f1)

0.31.0 (2026-05-20)

Breaking Changes

  • HassetteConfig restructured into nested groups — all configuration fields moved under group prefixes. Environment variables change from HASSETTE__LOG_LEVEL to HASSETTE__LOGGING__LOG_LEVEL, HASSETTE__DB_PATH to HASSETTE__DATABASE__PATH, etc. TOML/YAML config files and programmatic config.* access follow the same pattern (config.log_level → config.logging.log_level). Old flat keys are detected at startup with a deprecation warning but will not take effect — update your env vars, config files, and any direct HassetteConfig field access to the nested format. See src/hassette/config/legacy.py for the full old→new mapping. (#789)

Logging

  • Replace coloredlogs with a structlog-based logging pipeline — structured, context-rich log records with console and JSON formatters. All log I/O routed through an async queue so emit() never blocks the event loop. (#744)
  • Log records persisted to the telemetry database with configurable retention (log_retention_days) and persistence level (log_persistence_level). (#744)
  • New log viewer page in the web UI with server-side level filtering, search, column picker, and expand-to-detail. (#744)

Scheduler

  • if_exists="replace" option on all scheduler registration methods — when a job with the same name already exists, "replace" cancels the old job and registers the new one in its place. Useful when job configuration changes between app reloads. (#780)

Web UI

  • Complete UI redesign with apps-focused layout — sidebar navigation with live status dots, collapsible status groups, multi-instance app expansion, and command palette (Ctrl+K / Cmd+K). New app detail tabs: overview, handlers, code, config, and per-app logs. Global pages: cross-app handler/job table, diagnostics, and session history. (#710)
  • Real-time updates for handler/job invocation counts, health metrics, last-fired timestamps, and dashboard stats via WebSocket — no page refresh needed. Relative timestamps ("5m ago") tick forward on a 30-second interval. (#735)
  • Handler health table replaced with responsive card grid — each card shows status, handler name, kind, run stats (count, avg duration, error rate, last active), and error details. Keyboard accessible, responsive columns, scroll after 3 rows. (#761)
  • Consistent table pattern across apps, handlers, and logs pages with shared sort headers, inline column filter popovers, and unified table shell. (#767)
  • Multi-instance app parent page now shows shared tabs (overview, code, logs, config) with an instance column in the logs tab, instead of only an instance card grid. (#753)
  • API errors surfaced via toast notifications (sonner) instead of silently swallowed — users now see a notification when data fails to load. (#751)
  • Real-time handler updates on overview tab with failing-row background tint and blended hover state. (#748)

Configuration

  • HassetteConfig organized into 8 nested groups: DatabaseConfig, WebSocketConfig, LoggingConfig, LifecycleConfig, WebApiConfig, AppConfig, SchedulerConfig, FileWatcherConfig. The /api/config REST endpoint and frontend config page reflect the nested structure. (#789)

Bug Fixes

  • Handler cancel-then-resubscribe race and replacement ordering bugs fixed — Bus routing operations now execute synchronously in deterministic order, eliminating task interleaving between cancel and re-register. (#785, #658, #781)
  • Log table REST API level filter fixed — was sending exact-match filter to the server instead of fetching all levels; column picker checkbox alignment, filter panel overflow, and mobile "reset filters" button also fixed. (#755)
  • PyPI package now includes SPA frontend assets — pip install hassette was missing the web UI since v0.30.0. Also backported to v0.30.1. (#790, #788)

0.30.0 (2026-05-10)

Breaking Changes

  • Field type narrowing (StrEnum) — Fields previously typed as str are now typed with their domain StrEnum. StrEnums accept string construction so state.attributes.hvac_mode == "heat" still works, but type checkers will flag comparisons against raw strings. Affected fields: ClimateAttributes.hvac_action, hvac_mode, hvac_modes; LightAttributes.color_mode, supported_color_modes. (#716)
  • Field type changes (runtime) — MediaPlayerAttributes.media_position_updated_at: str | None → ZonedDateTime | None (validator now parses the ISO string). ClimateAttributes.current_temperature: int | float | None → float | None. (#716)
  • Removed deprecated fields — Fields typed as None in HA source (deprecated) are no longer declared on the model. Values still land in model_extra (accessible via .extra("field_name")): LightAttributes.color_temp, max_mireds, min_mireds, xy_color; MediaPlayerAttributes.entity_picture_local. (#716)
  • Removed constraints — LightAttributes.brightness no longer has gt=-1, lt=256 field validators. (#716)
  • hassette.models.states.features deleted — IntFlag enums moved to per-domain files. All enums are still re-exported from hassette.models.states so from hassette.models.states import LightEntityFeature continues to work. (#716)

State Models

  • State model attributes are now code-generated from Home Assistant core source via AST analysis, replacing the previous hand-maintained models. 34 entity domains have generated state models with proper @field_validator for ZonedDateTime wire conversion, StrEnum attribute types, and supports_* boolean properties. (#716)
  • The codegen pipeline includes a CI freshness check — if the pinned HA version falls behind, CI warns so models stay aligned with upstream. (#716)

0.29.0 (2026-05-09)

Breaking Changes

  • state.value type change for toggle entities — state.value for light, switch, fan, automation, humidifier, remote, script, siren, and update entities now returns bool instead of str. Code using state.value == "on" must change to state.value is True. (#711)

State Models

  • Audited all 50 BaseState subclasses against HA core source — corrected value types (9 toggle entities → BoolBaseState, 5 timestamp entities → DateTimeBaseState, air_quality → NumericBaseState), added missing attributes (person location fields, timer transitions, valve position, script/cover/device_tracker fields), and removed stale attributes not present in HA core. (#711)

Web UI

  • Handler and condition summaries in the dashboard now display human-readable text (e.g., entity light.kitchen and state → on) instead of Python repr output. (#700)
  • Service readiness (not just status) now visible in the frontend monitoring dashboard — services show whether they are actually ready to serve, not just that they started. (#688)

Bug Fixes

  • WebSocket CancelledError no longer swallowed during task cancellation (#682)
  • REST API returns 404 (not 500) for non-existent app_key on start/stop/reload endpoints (#603, #680)
  • Immediate-fire synthetic events now display "---" instead of a misleading UUID in telemetry (#640, #680)

0.28.0 (2026-05-02)

Breaking Changes

  • HassettePayload.event_id changed from int to str (UUID4) — any user code comparing event.payload.event_id against integers will silently fail. Update comparisons to use string UUIDs. (#641)

Telemetry

  • Every handler invocation and job execution now receives a globally unique execution_id (UUID4) for end-to-end tracing, with the triggering event's context_id and origin captured alongside it. (#641)
  • CURRENT_EXECUTION_ID ContextVar is set for the duration of each execution, enabling future causal chain tracking. (#641)
  • Frontend shows "Trace ID", "Trigger", and "Origin" columns in handler invocation and job execution tables — truncated monospace UUIDs with full value on hover. (#641)

0.27.0 (2026-04-29)

Service Supervision

  • Per-service RestartSpec replaces the global one-size-fits-all restart policy — each service declares its own restart type (PERMANENT, TRANSIENT, or TEMPORARY), sliding-window budget, backoff parameters, and error classification. The 5 global service_restart_* config fields are removed. (#638)
  • EXHAUSTED_DEAD and EXHAUSTED_COOLING service statuses with frontend rendering including a countdown timer for cooling services. (#638)

WebSocket Resilience

  • Early-drop retry loop detects post-ready connection drops within a configurable stable window (default 30s) and retries transparently — Home Assistant restarts no longer burn the ServiceWatcher restart budget or crash hassette after 5 cycles. (#631)
  • Total recovery timeout cap (default 5 minutes) prevents multiplicative worst-case retry windows. 8 new config fields externalize all retry parameters. (#631)

Bug Fixes

  • App bootstrap now completes before the app is marked ready, preventing premature requests against partially-initialized apps (#635)

0.26.0 (2026-04-28)

Bug Fixes

  • Graceful shutdown no longer crashes on ClosedResourceError (#627)
  • ZonedDateTime values in REST API responses no longer include IANA timezone suffix ([America/Chicago]), fixing URL interpolation in get_history and get_logbook (#619, #626)
  • TaskBucket crash logs now show meaningful task names (e.g., AppLifecycleService.bootstrap_apps) instead of Task-42 (#623, #626)
  • wait_for utility supports async predicates, eliminating hand-rolled async polling loops (#622, #626)
  • AppTestHarness per-class lock narrowed for concurrent same-class harnesses — multiple tests using the same app class can now run in parallel (#614)

0.25.0 (2026-04-26)

Breaking Changes

  • Scheduler API redesigned around trigger objects — run_cron("0 7 * * 1-5"), run_daily(at="07:00") replace the old keyword-per-field API. Custom triggers must implement TriggerProtocol. See the updated scheduler docs. (#517)
  • RecordingApi.get_entity requires explicit model argument — the previous BaseState-sentinel default silently aliased to get_state() and hid real bugs. Callers that want registry-converted state without a specific entity model should call get_state(entity_id) instead. (#525)
  • Docker startup uses constraints-based dependency protection — runtime uv sync replaced with a constraints file generated at build time. User dependency installs that conflict with hassette's pinned version now error instead of silently downgrading the framework. (#480)

Test Utilities

  • AppTestHarness — async context manager that wires a user's App class into test Bus, Scheduler, StateManager, and RecordingApi with zero boilerplate: async with AppTestHarness(MyApp, config={...}) as harness: (#492)
  • RecordingApi records write calls for assertions and delegates reads to StateProxy, with ApiProtocol conformance checking at import time (#492)
  • Time control for scheduler tests: freeze_time(), advance_time(), trigger_due_jobs() via custom _TestClock (#492)
  • Event simulation: simulate_state_change, simulate_attribute_change, simulate_call_service with reliable task-bucket drain (#492, #525)
  • State seeding: set_state, set_states via StateProxy._test_seed_state() (#492)

Scheduler

  • Built-in trigger types: After, Once, Every, Daily, Cron — all importable from hassette.scheduler.triggers (#517)
  • Job groups (group=, cancel_group(), list_jobs(group=)) and jitter support (jitter=) (#517)
  • Frontend shows structured trigger labels, group filter chips (URL-persisted), jitter tags, cancelled badges, and expandable job rows with "Next: fires in..." countdown (#517)

Bus

  • immediate=True fires handlers at registration time when the target entity already matches predicates. Synthetic event has old_state=None. Composes with once, debounce, throttle. (#570)
  • duration=N holds require the entity to remain in the matching state for N continuous seconds before the handler fires. Consults last_changed on restart for resilience. (#570)
  • Bus.on_error(handler) and Scheduler.on_error(handler) register app-level error handlers; per-registration on_error= parameter takes priority. Both sync and async handlers supported. (#575)

Error Handling

  • Execution timeout enforcement (600s default) for all scheduled jobs and event handlers via asyncio.timeout() — timed_out shown as a distinct status in telemetry with amber warning badges (#552)
  • timeout and timeout_disabled parameters on all Scheduler and Bus registration methods (#552)
  • Error tracebacks visible in the dashboard error feed with expand/collapse toggle; framework-tier errors separated from app-tier errors (#537)

API

  • Typed Home Assistant helper CRUD methods for input_boolean, input_number, input_select, input_text, input_datetime, counter, and timer entities (#506)

Web UI

  • Dashboard hierarchy redesign with app detail polish, quiet-canvas status indicators, and sorted grids (#485, #523)
  • Listener and job registrations now persist across session restarts — telemetry no longer loses data on reconnect (#466, #487)

Bug Fixes

  • Subscription race between cancel() and in-flight dispatch fixed; cancel semantics prevent double-fire (#451, #518, #520)
  • Dashboard no longer shows zero handlers/jobs after page refresh (#578)
  • Docker uv cache pruned on startup to prevent unbounded volume growth (#542)
  • SIGTERM handled for graceful Docker shutdown (#479)

Documentation

  • Comprehensive docs rewrite with scheduler parameter tables, troubleshooting index, security admonition for unauthenticated web API, and simplified getting-started first app (#507)
  • System internals page with architecture deep-dive diagrams (#605)

0.24.0 - 2026-04-03

Breaking Changes

  • TriggerProtocol split — custom triggers must now implement both first_run_time(current_time) and next_run_time(previous_run, current_time) instead of a single method. Triggers are now stateless. (#452)
  • /api/healthz removed — update Docker Compose health checks from /api/healthz to /api/health, which returns structured JSON and 503 for non-ok status. (#448)
  • Scheduler enforces job name uniqueness — duplicate job names per instance raise ValueError. Pass if_exists="skip" for idempotent registration. (#297)
  • once=True + rate limiting raises ValueError — combining once=True with debounce or throttle is no longer silently accepted. Remove the rate-limiting parameter from affected listeners. (#430)

Web UI

  • Complete rebuild as a Preact SPA with 5 pages: Dashboard, Apps, App Detail, Sessions, Logs (#343)
  • Graphite + Emerald design token system with light/dark mode toggle and [data-theme] persistence (#343, #442)
  • Session list page with status badges, "This Session" / "All Time" scope toggle, and localStorage persistence (#464)
  • Source location display in handler/job detail panels and log table (#464, #410)
  • Handler/job drill-down with invocation history, plain-language handler summaries, and error feed with exception type names (#343, #411)
  • Dashboard app cards show invocation/execution counts, error rate percentage, instance count badge, and multi-instance telemetry aggregation (#392, #410, #411, #448)
  • Multi-column sort on log table with live-streaming auto-pause and click-to-expand messages (#411, #381)
  • Keyboard accessibility: focus-visible indicators, skip-nav link, ARIA roles, keyboard-navigable log table (#442, #390)
  • Self-hosted fonts (DM Sans, JetBrains Mono, Space Grotesk) — no external CDN requests (#442)
  • Real-time log streaming via WebSocket with server-side level filtering and deduplication (#409)
  • WebSocket reconnection automatically refreshes all REST-fetched data without page reload (#379)
  • Status bar "DB degraded" indicator with exponential backoff polling (#448)

Database & Telemetry

  • Persistent SQLite telemetry storage for sessions, handler invocations, and job executions with automatic schema migrations (#305, #329)
  • Configurable retention (db_retention_days) and size limit (db_max_size_mb, default 500 MB) with automatic oldest-record deletion (#464)
  • Telemetry status endpoint (/api/telemetry/status) returns 503 when degraded, usable by Docker HEALTHCHECK (#448)
  • /api/health returns 503 for non-ok system status while preserving structured JSON (#448)

Bus & Scheduler

  • Rate limiter redesign: throttle no longer blocks concurrent dispatch, debounced handlers produce accurate telemetry, once=True listeners cannot double-fire under rapid events (#430)
  • Zero or negative debounce and throttle values rejected with ValueError at registration (#430)
  • if_exists parameter on all Scheduler.run_* methods — "error" (default) or "skip" for idempotent registration (#297)
  • IntervalTrigger rejects zero/negative intervals with ValueError at construction (#452)

Logging

  • Per-service log level tuning via 13 dedicated *_log_level config fields (e.g., api_log_level, bus_service_log_level) — no service falls through to global default (#463)

Configuration

  • total_shutdown_timeout_seconds — caps total shutdown wall-clock time (default: 30s) (#453)
  • db_path, db_retention_days, db_max_size_mb — telemetry database location, retention, and size limit (#305, #464)

Bug Fixes

  • Startup race condition: phased startup ensures session exists before handlers fire, eliminating "Dropping N handler invocation record(s)" warnings (#343)
  • CommandExecutor startup crash resolved — registration methods wait for DatabaseService readiness before DB access (#330)
  • SQLite write serialization eliminates OperationalError: cannot commit transaction races at startup (#333)
  • Stale listener/job registrations cleaned up on app restart; telemetry no longer shows removed handlers (#390)
  • Connection status bar no longer flashes "Disconnected" on page refresh (#390)
  • WebSocket hook no longer causes infinite reconnect loop (#379)
  • App start/stop/reload endpoints work in production mode without dev_mode flag (#390)
  • Strip literal quote characters from base_url, fixing Docker Compose connection failures (#298)
  • ServiceWatcher triggers shutdown on max restart failures instead of silently giving up (#301)
  • Scheduler auto-generated job names include trigger info, preventing name collisions (#446)
  • Scheduler job and listener filters return correct results after owner_id/app_key mismatch fix (#335, #336)

Documentation

  • Fixed broken code examples: run_hourly(minute=15) TypeError, invalid api_port field, wrong instance_name key in config snippet (#469, #472)
  • Added parameter tables to all scheduler methods with types, defaults, and cross-references (#469, #472)
  • Corrected data_dir default from ~/.hassette to actual platform-dependent path (#469, #472)
  • Added troubleshooting index page with symptom-based navigation (#469, #472)
  • Added security admonition for unauthenticated web API (#469, #472)
  • Extracted duplicated config file-discovery table into a shared snippet (#469, #472)
  • Simplified getting-started first app to use raw events; added typed-handler forward reference (#469, #472)
  • Removed ghost getting-started/configuration.md page (#469, #472)

[0.23.0] - 2026-02-19

Changed

  • Replaced Bulma CSS framework with a custom ht- prefixed design system featuring cool slate surfaces, warm amber accent, and Space Grotesk + JetBrains Mono typography (#262)
  • Extracted all design tokens into tokens.css with [data-theme] selector support for future theming (#262)
  • Redesigned dashboard with app status chip grid, activity timeline, and streamlined layout (#262)
  • App detail pages now use a flat single-page layout with collapsible metadata, inline tracebacks, and instance switcher dropdown (#262)
  • Bus listener and scheduler job tables show expanded detail rows with predicate, rate-limiting, and trigger information (#262)
  • Replaced hardcoded CSS fallback colors in alerts and detail panels with proper design tokens (--ht-surface-inset, --ht-surface-code, --ht-warning-*, --ht-danger-*)
  • Toggle buttons now show fallback text before Alpine.js initializes and expose aria-expanded for accessibility (#262)
  • E2E tests now run by default with uv run pytest instead of requiring -m e2e; added nox -s e2e session for CI
  • HassetteHarness now uses a fluent builder API (with_bus(), with_state_proxy(), etc.) with automatic dependency resolution instead of boolean flags (#253)
  • Consolidated duplicate mock Hassette, DataSyncService, and web test helper fixtures into shared factories in test_utils/ (#253)
  • All test helper functions now exported from hassette.test_utils public API; tests import from the package instead of submodules (#253)
  • Replaced 28 asyncio.sleep() synchronization calls across 8 integration test files with wait_for polling helper for deterministic, faster tests (#253)
  • Renamed create_mock_hassette() to create_hassette_stub() and mock_hassette.py to web_mocks.py to clarify web/API stub vs harness distinction (#259)
  • Added autouse cleanup fixtures for bus, scheduler, and mock API to prevent test pollution in module-scoped fixtures (#256)

Fixed

  • WebSocket service now fires disconnect event and marks not-ready immediately on unexpected connection loss, preventing stale state in StateProxy (#270)
  • App detail page now uses the actual instance index instead of hardcoded 0, fixing data/URL desync for non-zero instances (#262)
  • Detail panel labels now have proper text contrast on dark --ht-surface-code background (#262)
  • Collapsible panels and tracebacks no longer flash visible before Alpine.js initializes (#262)
  • Entity browser "Load more" button now appends rows instead of replacing existing ones on domain-filtered views (#247)
  • model_dump() and model_dump_json() on AppManifest and HassetteConfig no longer leak extra fields (e.g. tokens from environment variables)

Added

  • Aligned all state model attributes with Home Assistant core — added missing fields to sensor, humidifier, light, climate, weather, fan, camera, and media_player; created dedicated LockState module with LockAttributes and LockEntityFeature (#294)
  • supports_* boolean properties on light, climate, cover, fan, media_player, and vacuum attribute classes for checking entity capabilities without manual bitmask operations (#272)
  • IntFlag enums (LightEntityFeature, ClimateEntityFeature, etc.) matching Home Assistant core feature flags (#272)
  • Global alert banner showing HA disconnect warnings and failed app errors with expandable tracebacks (#262)
  • ht-btn--ghost and ht-btn--xs button modifier classes (#262)
  • extras property and extra() helper on BaseState and AttributesBase for safe access to integration-specific attributes (#271)
  • JSDoc comments across all web UI JavaScript files (#251)
  • ESLint linting, TypeScript type-checking, and mise run lint:js / mise run typecheck:js tasks (#251)

Removed

  • Bulma CSS CDN dependency (#262)
  • Entity Browser page and related partials (#262)

[0.22.1] - 2026-02-15

Added

  • Issue template migration from Markdown to YAML form templates (bug report, feature request, task, documentation)
  • /triage-issues Claude command for auditing and cleaning up GitHub issues against project conventions
  • Updated CLAUDE.md with GitHub Issues conventions (title, labels, milestones, body sections)
  • web_ui_hot_reload config option — watches web UI static files and templates for changes, pushing live reloads to the browser via WebSocket. CSS changes are hot-swapped without a page reload; template and JS changes trigger a full reload.
  • Collapsible sidebar with persistent icon rail on desktop and mobile — click the toggle or press Escape to expand/collapse
  • SPA-like page navigation via HTMX boost — page transitions without full reloads

Changed

  • Live dashboard/page updates now use WebSocket push with idiomorph DOM morphing instead of 30-second polling intervals

[0.22.0] - 2026-02-13

Added

  • Web UI — server-rendered monitoring dashboard at /ui/ using Jinja2, HTMX, Alpine.js, and Bulma CSS
  • Dashboard — system health, apps summary, bus metrics, and recent events with WebSocket-driven live updates
  • Apps page — shows all configured app manifests with status badges, start/stop/reload controls, and status filter tabs; single-instance apps link directly to instance detail
  • App detail (/ui/apps/{key}) — manifest config, bus listener metrics, scheduled jobs, and filtered log viewer; multi-instance apps show expandable instance table
  • Instance detail (/ui/apps/{key}/{index}) — per-instance bus listeners, jobs, and logs
  • Log viewer (/ui/logs) — client-side filtering by level/app/text, sortable columns, and real-time WebSocket log streaming
  • Scheduler page (/ui/scheduler) — scheduled jobs and execution history, filterable by app
  • Entity browser (/ui/entities) — browse entities by domain with text search and pagination
  • Event Bus page (/ui/bus) — bus listener metrics, filterable by app
  • run_web_ui config option to enable/disable the UI independently from the API
  • Added section to docs covering the web UI
  • FastAPI web backend replacing the standalone HealthService with a full REST API and WebSocket server
  • GET /api/health, GET /api/healthz — system health and container healthchecks
  • GET /api/entities, GET /api/entities/{entity_id}, GET /api/entities/domain/{domain} — entity state access
  • GET /api/apps, GET /api/apps/{app_key}, GET /api/apps/manifests — app status and manifests
  • POST /api/apps/{app_key}/start|stop|reload — app management
  • GET /api/scheduler/jobs, GET /api/scheduler/history — scheduled jobs and execution history
  • GET /api/bus/listeners, GET /api/bus/metrics — per-listener execution metrics and aggregate summary
  • GET /api/events/recent, GET /api/logs/recent, GET /api/services, GET /api/config — events, logs, HA services, config
  • GET /api/ws — WebSocket endpoint for real-time state/event/log streaming with subscription controls
  • GET /api/docs — interactive OpenAPI documentation
  • Event handler execution metrics — per-listener aggregate counters (invocations, successes, failures, DI failures, timing) exposed via REST API and web UI
  • Scheduler job execution history — per-job execution records with timing and error details
  • Configurable service restart with exponential backoff in ServiceWatcher
  • service_restart_max_attempts, service_restart_backoff_seconds, service_restart_max_backoff_seconds, service_restart_backoff_multiplier config options
  • scheduler_behind_schedule_threshold_seconds config option (default: 5) — configurable threshold before a "behind schedule" warning is logged for a job (previously hard-coded to 1 second)
  • Playwright e2e test suite for the web UI (34 tests; run with pytest -m e2e)

Changed

  • Breaking: Replaced HealthService with WebApiService backed by FastAPI
  • Breaking: Config renames: run_health_service → run_web_api, health_service_port → web_api_port, health_service_log_level → web_api_log_level
  • New config options: web_api_host, web_api_cors_origins, web_api_event_buffer_size, web_api_log_buffer_size, web_api_job_history_size
  • Service base class now properly sequences serve() task lifecycle: spawns after on_initialize(), cancels before on_shutdown()

Fixed

  • WebSocket disconnect handling no longer produces spurious ERROR logs during normal page navigation
  • Scheduler dispatch loop uses single lock acquisition per cycle, reducing scheduling latency

Removed

  • HealthService (src/hassette/core/health_service.py) — replaced by FastAPI web backend

[0.21.0] - 2026-02-06

Changed

  • Refactored AppHandler into four focused components: AppRegistry (state tracking), AppFactory (instance creation), AppLifecycleManager (init/shutdown orchestration), and AppChangeDetector (configuration diffing)
  • File watcher now batches multiple file change events to prevent race conditions (changed_file_path payload is now changed_file_paths: frozenset[Path])
  • Renamed active_apps_config to active_manifests on AppRegistry
  • AppManifest.app_config now accepts both "config" and "app_config" keys

Added

  • HassetteAppStateEvent emitted when app instances change status (includes app_key, status, previous_status, exception details)
  • New Bus convenience methods: on_app_state_changed(), on_app_running(), on_app_stopping()
  • BlockReason enum and blocked app tracking in AppRegistry to distinguish "enabled but excluded by @only_app" from "not configured"
  • ResourceStatus.STOPPING enum value
  • enabled_manifests property on AppRegistry for querying enabled apps regardless of only_app filter
  • StateManager.get(entity_id) for generic entity access with automatic domain-type resolution and BaseState fallback

Fixed

  • Removing @only_app decorator now correctly starts previously-blocked apps during hot reload

[0.20.4] - 2026-02-05

Fixed

  • Fixed finding of requirements files in Docker image, thanks @mlsteele!

Added

  • Added tests to ensure requirements files are found correctly in Docker image

[0.20.3] - 2026-02-01

Fixed

  • source now optional in AutomationTriggeredPayload

[0.20.2] - 2026-02-01

Changed

  • rename parameter comparator to op in Comparison condition

[0.20.1] - 2026-02-01

Fixed

  • add back activation of virtualenv in docker startup script

[0.20.0] - 2026-02-01

Added

  • Add --version/-v argument to Hassette to allow displaying the current version
  • Add __iter__, __contains__, keys, values, and items methods to StateManager and StateRegistry
  • Add functionality to route state_change events to more specific handlers based on domain and/or entity_id
  • This is done automatically by the Bus by adding the entity_id to the topic when creating the listener
  • Matched listeners are deduplicated to ensure delivery only happens one time
  • Events are dispatched to the most specific route if there are multiple matches
  • Add AnnotationConverter class and TypeMatcher class for more robust validation/conversion during DI
  • Add A, P, C, and D aliases to hassette.__init__ for simpler imports
  • A = hassette.event_handling.accessors
  • P = hassette.event_handling.predicates
  • C = hassette.event_handling.conditions
  • D = hassette.event_handling.dependencies
  • Add new Comparison condition for basic operators (e.g. ==, !=, <, >, etc.) to compare values in state/attribute change listeners
  • Add new accessors for getting multiple/all attributes at once from state change events
  • get_attrs_<old|new|old_new> - specify a list of attrs
  • get_all_attrs_<old|new|old_new> - get all attributes as a dict
  • Add get_all_changes accessor that returns a dictionary of all changes, including state and all attributes

Fixed

  • Fix AppHandler reporting failed apps as successful by using status attribute
  • This is due to some issues with how we're tracking apps, further fixes will need to happen in future releases
  • Fix StateManager using BaseState when we do not find a class in the StateRegistry
  • This does not work because BaseState doesn't have a domain
  • Error is now raised instead
  • Log level is now used by Apps if set directly in AppConfig in Python code (as opposed to config file)
  • Fix HassPayload's context attribute not being a HassContext instance
  • MediaPlayerState now has attributes using the correct type

Changed

  • BREAKING: Replaced StateManager.get_states with __getitem__ that accepts a state class
  • The error raised in StateManager when a state class is not found in the StateRegistry now advises to use this method
  • Renamed LOG_LEVELS to LOG_LEVEL_TYPE
  • Renamed get_default_dict to get_defaults_dict to be more clear this is not referring to defaultdict class
  • Use same validation for AppConfig log level as we do for Hassette config log level
  • Extracted nested Attributes classes for each state out of class definition to make them first class citizens
  • e.g. MediaPlayerState.Attributes is now MediaPlayerAttributes

Docs

  • Remove Why Hassette page
  • Remove docker networking page
  • Very large cleanup/reorg/addition of docs

[0.19.2] - 2026-01-25

Fixed

  • Change log level for state cache loading message from INFO to DEBUG

[0.19.1] - 2026-01-25

Fixed

  • Update state_manager.pyi to fix type hints

[0.19.0] - 2026-01-25

Fixed

  • Exit TypeRegistry.convert early if already a valid type
  • Avoid mutating state dicts when accessing via DomainStates

Added

  • Add __contains__ method to DomainStates
  • Allows us to use in checks
  • Add to_dict, keys, values, and items methods to DomainStates
  • Provides convenient access to entity IDs and typed states
  • Add yield_domain_states to StateProxy
  • Allows iterating over all states in the proxy
  • Handles KeyError when extracting domain
  • Update DomainStates class to accept a StateProxy instance instead of state dictionary to ensure it stays up to date
  • Add caching to StateManager, holding on to each DomainStates instance after creation
  • Add caching to DomainStates, using frozendict.deepfreeze to hash the state dict and avoid recreating the instance if it has not changed

Removed

  • BREAKING: Remove _TypedStateGetter class and corresponding get method on StateManager - this was never a good idea due to its confusing api
  • BREAKING: Remove all property on StateManager - this is to avoid calculating all states unnecessarily

[0.18.1] - 2025-12-13

Changed

  • Improve docker startup script and dependency handling
  • Rewrite docker docs to be more clear about project structure and dependency installation

Fixed

  • Fixed a bug in autodetect apps exclusion directories
  • Previous commit had mapped the exclusion dirs to Path objects, which broke the set comparison, this has been reverted

[0.18.0.dev3] - 2025-12-13

Changed

  • Hardcode UID/GID of 1000 for non-root user in Docker image

[0.18.0.dev2] - 2025-12-13

Changed

  • Breaking: Docker image switched to Debian slim
  • Breaking: Remove latest tag, latest tag will now include python version as well

Fixed

  • Use correct version of python when pulling base image
  • (e.g. image tagged with py-3.12 uses python 3.12)

[0.18.0.dev1] - 2025-12-13

Changed

  • Allow Python 3.11 and 3.12 again!
  • Breaking: All events now contain untyped payloads instead of typed ones
  • StateChangeEvent is now RawStateChangeEvent
  • There is a new DI handler for TypedStateChangeEvent to handle conversion if desired
  • Breaking: State conversion system now uses dynamic registry instead of hardcoded unions
  • StateUnion type has been removed - use BaseState in type hints instead
  • DomainLiteral type has been removed - no longer needed with dynamic registration
  • State classes automatically register their domains via __init_subclass__ hook
  • Breaking: try_convert_state now typed to return BaseState | None instead of StateUnion | None
  • Uses registry lookup instead of Pydantic discriminated unions for conversion
  • Falls back to BaseState for unknown/custom domains
  • try_convert_state moved to hassette.state_registry module
  • states.__init__ now only imports/exports classes, no conversion logic
  • Improved dependency injection system for event handlers, including support for optional dependencies via Maybe* annotations
  • Renamed states.py to state_manager.py (and renamed the class) to avoid confusion with models/states module
  • Removed defaults from StateT and StateValueT type vars
  • Removed type constraints from StateValueT type var to allow custom types to be used
  • Moved accessors, conditions, dependencies, and predicates all to hassette.event_handling for consistency
  • Moved DI extraction and injection modules to hassette.bus

Added

  • TypeRegistry class for handling simple value conversion (e.g. converting "off" to False)
  • Handling of Union types
  • Handling of None types
  • Handling of type conversion for custom Annotated DI handlers

Removed

  • Breaking: Removed StateUnion type - replaced with BaseState throughout codebase
  • Breaking: Removed DomainLiteral type - no longer needed with registry system
  • Breaking: Removed manual _StateUnion type definition from states module
  • Breaking: Removed StateValueOld/New, StateValueOldNew, StateOldNew, MaybeStateOldNew, AttrOld, AttrNew, AttrOldNew DI handlers
    • These can be used still by annotating with Annotated[<type>, A.<function>] using provided accessors module
    • They were too difficult to maintain/type properly across the framework

[0.17.0] - 2025-11-22

Changed

  • Breaking: - Requires Python 3.13 going forward, Python 3.12 and 3.11 are no longer supported.
  • This allows use of type, defaults for TypeVars, and other new typing features.
  • Renamed core_config.py to core.py
  • Renamed services to core and move core.py under core directory
  • Didn't make sense to keep named as services since we have resources in here as well

Added

  • Add diskcache dependency and cache attribute to all resources
  • Each resource class has its own cache directory under the Hassette data directory
  • Add states attribute to App - provides access to current states in Home Assistant
  • states is an instance of the new States class
  • States provides domain-based access to entity states, e.g. app.states.light.get("light.my_light")
  • States listens to state change events and keeps an up-to-date cache of states
  • New states documentation page under core-concepts
  • Add Maybe* DI annotations for optional dependencies in event handlers
  • MaybeStateNew, MaybeStateOld, MaybeEntityId, etc.
  • These will allow None or MISSING_VALUE to be returned if the value is not available
  • The original dependency annotations will raise an error if the value is not available
  • Add raise_on_incorrect_dependency_type to HassetteConfig to control whether to raise an error if a dependency cannot be provided due to type mismatch
  • Default is true in production mode, false in dev mode
  • When false a warning will be logged but the handler will still be called with whatever value was returned

Fixed

  • Fixed bug that caused apps to not be re-imported when code changed due to skipping cache check in app handler
  • Fixed missing domains in DomainLiteral in hassette.models.states.base
  • Add tests to catch this in the future

[0.16.0] - 2025-11-16

Added

  • Added ANY_VALUE sentinel for clearer semantics in predicates - use this to indicate "any value is acceptable"
  • Dependency Injection for Event Handlers - Handlers can now use Annotated type hints with dependency markers from hassette.dependencies to automatically extract and inject event data as parameters. This provides a cleaner, more type-safe alternative to manually accessing event payloads.
  • Available dependencies include StateNew, StateOld, AttrNew(name), AttrOld(name), EntityId, Domain, Service, ServiceData, StateValueNew, StateValueOld, EventContext, and more
  • Handlers can mix DI parameters with custom kwargs
  • See hassette.dependencies module documentation and updated examples for details

Changed

  • Breaking: - Event handlers can no longer receive positional only args or variadic positional args
  • NOT_PROVIDED predicate is now used only to indicate that a parameter was not provided to a function

[0.15.5] - 2025-11-14

Changed

  • Update HassetteConfig defaults to differ if in dev mode
  • Generally speaking, values are extended (e.g. timeouts) and more permissive (e.g. allow_startup_if_app_precheck_fails = true in dev mode)
  • Moved AppManifest and HassetteTomlConfigSettingsSource to classes.py
  • Moved LOG_LEVELS to hassette.types.types instead of const.misc, as this is a Literal, not a list of constants
  • Renamed core_config.py to core.py
  • Bumped version of uv in mise.toml, docker image, and build backend
  • Converted docs to mkdocs instead of sphinx

Fixed

  • Fixed bug in AppHandler where all apps would be lost when handle_changes was called, due to improper reloading of configuration
  • Now uses HassetteConfig.reload() to reload config instead of re-initializing the class

[0.15.4] - 2025-11-07

Added

  • add config setting for continuing startup if app precheck fails
  • add config setting for skipping app precheck entirely
  • add config setting for loading found .env files into os.environ
  • add entities back to public API exports from hassette

Changed

  • Cache app import failures to avoid attempting to load these again if we are continuing startup after precheck failures
  • Improve app precheck logging by using logger.error and short traceback instead of logger.exception

[0.15.3] - 2025-11-02

Changed

  • Moved more internal log lines to DEBUG level to reduce noise during normal operation.
  • Moved only_app warning to only emit if @only_app is actually being used.
  • Make FalseySentinel subclass to use for NOT_PROVIDED and MISSING_VALUE to simplify bool checks.
  • Add Typeguard method to StateChangePayload to allow type narrowing on old_state and new_state.
  • Implemented as self.has_state(<self.old_state|self.new_state>)

Documentation

  • Improved documentation landing page
  • Add logo
  • Improve getting-started page

[0.15.2] - 2025-11-02

Fixed

  • Fix docker_start.sh to use new entrypoint

[0.15.0] - 2025-11-02

Added

  • ComparisonConditions for comparing old and new values in state and attribute change listeners.
  • Increased and Decreased conditions added for numeric comparisons.
  • Added IsNone and IsNotNone conditions for checking if a value is None or not.
  • Hassette will now attempt to automatically detect apps and register them without any configuration being required.
  • This can be disabled by setting auto_detect_apps = false in the config.
  • Manually configured apps will still be loaded as normal and take precedence over auto-detected apps.
  • You cannot use auto-detect apps if you have a configuration with required values (unless they are being populated from environment variables or secrets).
    • In this case, you must manually configure the app to provide the required values.

Fixed

  • Fixed missing tzdata in Alpine-based Docker image causing timezone issues.
  • Cli parsing working now, so any/all settings can be passed to override config file or env vars, using --help works correctly, etc.

Removed

  • Setting sources custom tracking removed, so debug level logging will no longer show where each config value was set from.
  • This was originally added due to my own confusion around config precedence, but maintaining it is not worth the extra complexity.
  • Secrets can no longer be set in hassette.toml to be accessible in app config
  • This never actually made much sense, I just didn't actually think about that when adding the feature

Changed

  • You can now pass ComparisonConditions to the changed parameter on on_state_change and on_attribute_change methods.
  • This allows for comparing the old and new values to each other, rather than checking each independently.
  • You are now able to register event handlers that take no arguments, for events where you don't care about the event data.
  • The handler will simply be called without any parameters when the event is fired.
  • This works for all bus listener methods, e.g. on_event, on_entity, on_status_change, etc.
  • When you do require the event to be passed, you only need to ensure it is the first parameter and the name is event.
  • No longer export anything through predicates module
  • Recommendation now is to import like
    • from hassette import predicates as P
    • from hassette import conditions as C
    • from hassette import accessors as A
  • Breaking: - base_url now requires an explicit schema (http:// or https://)
  • If no schema is provided, a SchemeRequiredInBaseUrlError will be raised during config validation
  • This is to avoid having to guess the intended scheme, which can lead to confusion and errors
  • Breaking: - base_url must have port included if your instance requires the port
  • Previously, we would default to port 8123 if no port was provided
  • This is not always correct, as some instances may be running on a different port, be behind a reverse proxy, or use nabu casa and not require a port at all

Internal

  • Refactor listener and add adapter to handle debounce, throttle, and variadic/no-arg handlers more cleanly.
  • Rename Hasette._websocket to Hassette._websocket_service to match naming conventions.
  • Refactor handler types and move types into types module instead of single file for better organization.
  • Remove extra wrappers around pydantic-settings, made some improvements so these are no longer necessary.
  • Flattened whole package structure for simpler imports and better organization.

[0.14.0] - 2025-10-19

Added

  • Add validation for filename extension in AppManifest - add .py if no suffix, raise error if not .py
  • Bus handlers can now accept args and kwargs to be passed to the callback when the event is fired
  • tasks.py renamed to task_bucket.py to follow naming conventions
  • post_to_loop method added to TaskBucket to allow posting callables to the event loop from other threads

Changed

  • Breaking: - Renamed async_utils.py to func_utils.py, added callable_name and callable_short_name utility functions
  • Breaking: - Upgrade to whenever==0.9.* which removed SystemDateTime in favor of ZonedDateTime - all references in the code base have been updated

Internal

  • New type for handlers, HandlerType, as we now have additional protocols for variadic handlers

Fixed

  • Correct scheduler helpers run_minutely, run_hourly, and run_daily to not start immediately if no start was provided, but to start on the next interval instead.

Bus/Predicate Refactor

  • Refactored predicates to use composable Predicates, Conditions, and Accessors
  • Predicate is a callable that takes an event and returns a bool
    • E.g. AttrFrom, AttrTo, DomainMatches, EntityMatches, ValueIs, etc.
  • Condition is a callable that takes a value and returns a bool
    • E.g. Glob, Contains, IsIn, IsOrContains, Intersects, etc.
  • Accessor is a callable that takes an event and returns a value
    • E.g. get_domain, get_entity_id, get_service_data, get_path, etc.
  • Updated Bus methods to use new predicate system
  • Only implementation changes, public API remains the same
  • Updated tests to use new predicate system
  • Add/update types for predicates, conditions, and accessors
  • Updated documentation for predicates and bus event listening to reflect new system

[0.13.0] - 2025-10-14

Added

  • Subscription now has cancel method to unsubscribe from events, to be consistent with ScheduledJob.
  • App.send_event_sync method added for synchronous event sending.
  • Bus.on_status_change, Bus.on_attribute_change, Bus.on_service_call all take sync callables for comparison parameters.
  • For example, you can pass a lambda to changed_from that does a custom comparison.
  • Bus now exposes on_homeassistant_stop and on_homeassistant_start convenience methods for listening to these common events.
  • Bus status/attribute change entity_id parameters now accept glob patterns.

Changed

  • Breaking: Scheduler.run_once has been updated to use start instead of run_at to be consistent with other helpers.
  • Breaking: cleanup method is now marked as final and cannot be overridden in subclasses.
  • Breaking: Bus.on_entity renamed to Bus.on_status_change to match naming conventions across the codebase.
  • Breaking: Bus.on_status_change entity parameter renamed to entity_id for clarity.
  • Breaking: Bus.on_attribute renamed to Bus.on_attribute_change to match naming conventions across the codebase.
  • Breaking: Bus.on_attribute_change entity parameter renamed to entity_id for clarity.

Removed

  • Breaking: Removed deprecated set_logger_to_debug and set_logger_to_level Resource methods.
  • Breaking: Removed deprecated run_sync, run_on_loop_thread, and create_task methods from Hassette.
  • Breaking: Removed run_at alias for run_once in Scheduler.

Internal

  • Remove scheduled jobs that are cancelled or do not repeat, instead of just marking them as cancelled and leaving them in the job queue.
  • Reworked predicates to make more sense and be more composable.
  • Added types for PredicateCallable, KnownTypes, and ChangeType.
    • PredicateCallable is a callable that takes a single argument of any known type and returns a bool.
    • KnownTypes is a union of all types that can be passed to predicates.
    • ChangeType is a union of all types that can be passed to change parameters.
  • Use Sentinel from typing_extensions for default values.
  • Rename SENTINEL to NOT_PROVIDED for clarity.
  • Moved is_async_callable to hassette.utils.async_utils, now being used in more places.
  • Moved glob logic from Router to hassette.utils.glob_utils, now being used in more places.

Documentation

  • Updated Apps and Scheduler documentation to reflect new features and changes.
  • Improved reference docs created with autodoc.

[0.12.1] - 2025-10-11

Fixed

  • Fixed run_minutely/run_hourly/run_daily scheduler helpers to run every N minutes/hours/days, not every minute/hour/day at 0th second/minute.

[0.12.0] - 2025-10-11

Added

  • Lifecycle:
  • Lifecycle hooks on/before/after_initialize and on/before/after_shutdown added to Resource and Service for more granular control over startup and shutdown sequences.
  • Breaking: App.initialize and App.shutdown are now final methods that call the new hooks; attempting to override them will raise a CannotOverrideFinalError.
  • Developer Experience:
    • Hassette now performs a pre-check of all apps before starting, exiting early if any apps raise an exception during import.
    • This allows earlier iteration for exceptions that can be caught at class definition/module import time.
    • Scheduler now includes convenience helpers run_at, run_minutely, run_hourly, and run_daily for common cadence patterns.
    • Add humanize to support human-friendly duration strings in log messages.
  • Dev Mode:
  • Reintroduced dev_mode configuration flag (also auto-enabled when running under a debugger or python -X dev) to turn on asyncio debug logging and richer task diagnostics.
  • Only reload apps when in dev_mode, to avoid unexpected reloads in production, overridable with always_reload_apps config flag.
  • Only respect @only_app decorator when in dev_mode, to avoid accidentally running only one app in production - overridable with allow_only_app_in_prod config flag.
  • The event loop automatically switches to debug mode when dev_mode is enabled.
  • Task Buckets:
  • Task buckets gained context helpers and run_sync/run_on_loop_thread wrappers so work spawned from worker threads is still tracked and can be cancelled cleanly.
  • Task buckets now expose make_async_adapter, replacing the old helper in hassette.utils.async_utils so sync callables are wrapped with the owning bucket's executor.
  • App-owned Api, Bus, and Scheduler instances share the app's task bucket and derive unique name prefixes, giving per-instance loggers and consistent task accounting.
  • All Apps (and all resources/services) should use self.task_bucket to spawn background tasks and to run synchronous code, to ensure proper tracking and cancellation.
    • Using self.hassette.run_sync or self.hassette.run_on_loop_thread is still supported, but will not track tasks in the app's task bucket.
  • Configuration:
  • Resolve all paths in HassetteConfig to absolute paths.
  • Individual service log levels can be set via config, with the overall log_level being used if not specified.
  • New config options for individual service log levels:
    • bus_service_log_level
    • scheduler_service_log_level
    • app_handler_log_level
    • health_service_log_level
    • websocket_log_level
    • service_watcher_log_level
    • file_watcher_log_level
    • task_bucket_log_level
    • apps_log_level
    • Add log_level to AppConfig so apps can set their own log levels.
  • Add new configuration options for logging events on the Bus when at DEBUG level:
    • log_all_events - log every event that is fired.
    • log_all_hass_events - log every event from Home Assistant - will fall back to log_all_events if not set.
    • log_all_hassette_events - log every event from Hassette apps and core - will fall back to log_all_events if not set.
  • Add app_startup_timeout_seconds and app_shutdown_timeout_seconds to HassetteConfig to control how long to wait for apps to start and stop before giving up.
  • Allow having the Bus skip entities/domains altogether via bus_excluded_domains and bus_excluded_domains config options.
    • These take a tuple of strings and accept glob patterns.
    • Any events matching an excluded domain or entity_id will not be delivered to listeners or logged.

Changed

  • Breaking: Public imports now come from the root hassette package; the old hassette.core paths have been moved under hassette.core.resources / hassette.core.services, so update any direct hassette.core... imports to use the re-exported names on hassette.
  • Breaking: App.initialize and App.shutdown have been replaced with App.on_initialize and App.on_shutdown hooks that do not need to call super().
  • Attempting to override these methods will now raise a CannotOverrideFinalError.
  • The Scheduler will now spawn tasks to run a job and reschedule a job, so jobs that take longer than their interval will not block subsequent runs.
  • Resources now build a parent/child graph via Resource.add_child and harmonized create() factory methods, so services and sub-resources inherit owners and task buckets automatically.
  • Api.call_service and the sync facade default to return_response=False, and the turn_on / turn_off / toggle are corrected to not pass return_response since this is not supported.
  • Deprecated set_logger_to_level - loggers are finally working properly now so the standard logger.setLevel(...) should be used instead.

Fixed

  • Event bus and scheduler loops respect shutdown_event, allowing them to exit promptly during shutdown.
  • WebSocket reconnects treat CouldNotFindHomeAssistantError as retryable and properly apply the retry policy, improving cold-start resilience.
  • Api.call_service now includes return_response in the payload when requested, and ServiceResponse correctly models the returned data.

Internal

  • Improved documentation:
  • Switched to RTD theme for better readability and navigation.
  • Improved formatting of comparison guides.
  • Fixed some references.
  • Reorganized most of the core code into resources and services
  • Use contextvars instead of class variables to track global instance of Hassette and HassetteConfig
  • SchedulerService now delegates scheduling to _ScheduledJobQueue, which uses a fair async lock to coordinate concurrent writers before dispatching due jobs.
  • Hassette.run_sync/run_on_loop_thread now route through the global task bucket.
  • Breaking: The run_forever method of the Service class has been replaced with serve. The new lifecycle hooks are valid for Service as well.

[0.11.0] - 2025-10-05

Added

  • hassette.event.app_reload_completed now fires after reload cycles, and HassetteEmptyPayload provides a helper for simple internal events.
  • Add TaskBucket class for tracking and cancelling related async tasks.
  • Add Hassette.task_bucket for global task tracking, and Resource.task_bucket for per-resource task tracking.
  • Introduced TaskBucket instances for Hassette, services, and apps; configure shutdown grace periods via the new HassetteConfig.task_cancellation_timeout_seconds setting.
  • Added Hassette.wait_for_ready and hassette.utils.wait_for_ready helpers so resources can block on dependencies (for example, the API now waits for the WebSocket).
  • Add ResourceNotReadyError exception to indicate that a resource is not ready for use.
  • Expanded Home Assistant tuning knobs with websocket_connection_timeout_seconds, websocket_total_timeout_seconds, websocket_response_timeout_seconds, websocket_heartbeat_interval_seconds, and scheduler_min/default/max_delay_seconds.
  • Add individual log level settings for core services.
  • Add cleanup lifecycle method to Resource and Service for async cleanup tasks during shutdown. This generally will not need to be overridden, but is available if needed.

Changed

  • Breaking: Per-owner buses replace the global hassette.bus; listener removal must go through BusService, which now tracks listeners by owner under a fair async lock for atomic cleanup.
  • Breaking: @only becomes @only_app, apps must expose a non-empty instance_name, and each app now owns its Bus and Scheduler handles.
  • Breaking: The hassette.core.apps package moved under hassette.core.classes.app, and the service singletons are now BusService and SchedulerService; import apps from hassette.core/hassette.core.classes and treat the underscored services as private.
  • Deprecated: set_logger_to_debug has been renamed to set_logger_to_level, and all core services now default to INFO level logging. set_logger_to_debug is still available but will be removed in a future release.
  • App handlers now mark apps as ready after initialize completes.
  • The API now waits for WebSocket readiness before creating its session, and classifies common client errors as non-retryable.

Fixed

  • App reloads clean up owned listeners and jobs, preventing leaked callbacks between reload cycles.
  • Startup failures now emit the list of resources that never became ready, making it easier to diagnose configuration mistakes.

Internal

  • Test harness integrates TaskBucket support, adds a hassette_with_nothing fixture, and continues to provision mock services so CI can run without a Home Assistant container.
  • Tightened local tooling: expanded pyrightconfig.json, enabled Ruff's TID252, and taught the nox test session to run pytest with -W error.
  • Scheduler coordination now flows through SchedulerService, which reads min/default/max delays from config, waits for Hassette readiness, and tags spawned jobs in the task bucket for easier cancellation.
  • Lifecycle helpers extend Resource/Service with explicit readiness flags (mark_ready, mark_not_ready, is_ready); Hassette spins up a global task bucket, names every background task, and blocks startup until all registered resources report ready, logging holdouts before shutting down.
  • WebSocket connection handling uses Tenacity-driven retries with dedicated connect/auth/response timeouts, and the API now waits for WebSocket readiness before creating its session while classifying common client errors as non-retryable.
  • Add asyncio task factory to register all tasks in the global task bucket with meaningful names to make cleanup easier.

[0.10.0] - 2025-09-27

Added

  • Added utility functions for datetime conversion in src/hassette/utils.py

Changed

  • Updated state models to use SystemDateTime consistently instead of Instant or mixed types
  • Replaced deprecated InstantBaseState with DateTimeBaseState for better type handling
  • Remove repr=False for last_changed, last_updated, and last_reported in BaseState to improve logging and debugging output

Fixed

  • Fixed incorrect datetime conversion in InputDateTimeState to ensure proper timezone handling

[0.9.0] - 2025-09-26

Added

  • Added ability to provide args and kwargs to scheduled jobs via scheduler helpers
  • args and kwargs keyword-only parameters added to all scheduler helper functions
  • These will be passed to the scheduled callable when it is run
  • See Scheduler documentation for details

Changed

  • Narrow date/time types accepted by get_history, get_logbook, get_camera_image and get_calendar_events to exclude datetime, date, and ZonedDateTime - use PlainDateTime, SystemDateTime, or Date instead

Documentation

  • Updated scheduler documentation to include new args/kwargs parameters for scheduling helpers
  • Updated Readme to change roadmap reference to point to Github project board
  • Removed roadmap.md file, using project board for tracking now

[0.8.1] - 2025-09-23

Fixed

  • Remove opengraph sphinx extension from docs dependencies - it was causing issues with building the docs and isn't necessary for our use case

[0.8.0] - 2025-09-23

Added

  • hot-reloading support for apps using watchfiles library
  • watches app files, hassette.toml, and .env files for changes
  • reloads apps on change, removes orphans, reimports apps if source files change
  • can be disabled with watch_files = false in config
  • add a few new configuration values to control file watcher behavior
  • add utility function to wait for resources to be running with shutdown support
  • wait_for_resources_running function added to Hassette class
  • also available as standalone utility function in hassette.utils
  • @only decorator to allow marking a single app to run without changing hassette.toml
  • importable from hassette.core.apps
  • useful for development when you want to only run a single app without modifying config file
  • will raise an error if multiple apps are marked with @only
  • add app_key to AppManifest - reflects the key used to identify the app in config

Changed

  • move service watching logic to it's own service
  • refactor app_handler to handle reloading apps, re-importing, removing orphans, etc.

Fixed

  • update api.call_service target typing to also allow lists of ids - thanks @zlangbert!

[0.7.0] - 2025-09-14

Changed

  • rename cancel on Subscription to unsubscribe for clarity

Fixed

  • improved docstrings across Api methods

Added

  • Documentation!

[0.6.2] - 2025-09-14

Fixed

  • Fix logging on App subclasses to use hassette.<AppClassName> logger

[0.6.1] - 2025-09-14

Fixed

  • Fixed HassetteConfig using single underscore when checking for app_dir, config_dir, and data_dir manually
  • Now checks both single and double underscore (with double underscore taking precedence) just to be safe
  • Fixed HassetteConfig incorrectly prioritizing HASSETTE_LOG_LEVEL over HASSETTE__LOG_LEVEL (double underscore should take precedence)

[0.6.0] - 2025-09-14

Removed

  • Removed DEFAULT_CONFIG constant for app config, not necessary

Fixed

  • Fixed HassetteConfig to properly handle env_file and config_file parameters passed in programmatically or via CLI args
  • These are now passed to the appropriate settings sources correctly
  • Fixed HassetteConfig incorrectly prioritizing TomlConfig over environment variables and dotenv files (Pydantic docs are confusing on this point)

Changed

Configuration

  • Add back ability to set top level [hassette] section in config file using custom TomlConfigSettingsSource
  • Update examples to show top level [hassette] section usage
  • Update README with new config usage and Docker instructions
  • Update README with example of using docker-compose.yml file
  • Update README with example of setting app config inline (.e.g config = {send_alert = true})
  • Added relative ./config path for config and .env files

App Handler

  • Improved app handler logic, apps should now be able to import other modules from the same app directory
    • Known Issue: Using isinstance does not work consistently, will be providing recommendation in docs on how to make this work better

Hassette

  • Update imports to be relative, same as other modules

Apps

  • Rename app_manifest_cls to app_manifest - was always an instance, not a class

Added

HassetteConfig

  • Add secrets attribute to HassetteConfig to allow specifying secret names that will be filled from config sources
    • Secrets can be listed in the config file like secrets = ["my_secret", "another_secret"]
    • Secrets will be filled from config sources in order or will attempt to pull from environment variables if not found
    • Secrets are available in config as a dict, e.g. config.secrets["my_secret"]
  • Add HassetteBaseSettings to add tracking of final settings sources for all config attributes
    • HassetteConfig.FINAL_SETTINGS_SOURCES will show where each config attribute was set from
    • Useful for debugging config issues
  • Add HassetteTomlConfigSettingsSource to load config from a TOML file, supports top level [hassette] section
  • Add get_config class method to HassetteConfig to get global configuration without needing to access Hassette directly
    • E.g. HassetteConfig.get_config() will return the current config instance
  • Check for app required keys prior to loading apps, will skip any apps missing required keys and log a warning
    • Particularly useful if you have config values for the app in environment variables but have the app removed/disabled

Hassette

  • Surface get_app on Hassette class to allow getting an app instance by name and index (if necessary)
    • E.g. hassette.get_app("MyApp") or hassette.get_app("MyApp", 1)

[0.5.0] - 2025-09-12

Changed

  • BREAKING: Remove logic to pop top level [hassette] section from config file, this has the unfortunate side effect of potentially overriding values set in environment variables
  • Update examples to remove references to top level [hassette] section
  • Add warning if we detect this section in the config file
  • Add TODO to get this working by implementing a custom TomlConfigSettingsSource that handles this
  • BREAKING: Switch back to __ double underscore for environment variable prefixes, prevents issues with app config that uses single underscore
  • Add env_file to AppConfig default class config to load environment variables from /config/.env and .env files automatically
  • Add examples of using SettingsConfigDict to set a custom env_prefix on AppConfig subclasses

[0.4.2] - 2025-09-12

Fixed

  • Fixed permissions for /app and /data in Dockerfile
  • Update example docker-compose.yml to use named volume for /data

[0.4.1] - 2025-09-11

Fixed

  • Fixed Dockerfile to build for both amd64 and arm64

[0.4.0] - 2025-09-10

Added

Docker Support

  • Dockerfile with Python 3.12-alpine base image for lightweight deployment
  • Docker start script to set up virtual environment, install dependencies, and run Hassette
  • /apps that contain a pyproject.toml or uv.lock will be installed as a project
  • /config and /apps will be scanned for requirements.txt or hassette-requirements.txt files and merged for installation
  • Example docker-compose.yml file for easy setup
  • uv cache directory at /uv_cache to speed up dependency installation

Configuration

  • New app_dir configuration option to specify the directory containing user apps (default: ./apps)
  • Top level [hassette] can be used - previously had to be at the root of the file, with no header
  • HealthService config - allow setting port and allow disabling health service
  • health_service_port (default: 8126)
  • run_health_service (default: true)

Changed

  • BREAKING: Moved all event models from hassette.models.events to hassette.core.events for better organization
  • BREAKING: Updated configuration structure - flattened Hass configuration properties directly into main config
  • config.hass.token → config.token
  • config.hass.ws_url → config.ws_url
  • config.hass.base_url → config.base_url
  • BREAKING: Changed environment variable prefix from hassette__* to hassette_* (double underscore to single)
  • Change resource status prior to sending event, to ensure consistency
  • Improve retry logic in _Api and _Websocket classes

Fixed

  • Improved App constructor with better parameter formatting and documentation
  • Added index parameter documentation to App __init__ method
  • Fixed logging initialization to handle missing handlers gracefully using contextlib.suppress
  • Enhanced state conversion with better discriminated union handling using Pydantic's discriminator field
  • Improved error handling in try_convert_state function
  • Updated AppConfig to allow arbitrary types (arbitrary_types_allowed=True)
  • Handle bug in HealthService config - sometimes web.AppKey raises an UnboundLocalError (only seen in testing so far), fallback to string in this case

Removed

  • Removed unused _make_unique_name method from App class
  • Removed KNOWN_TOPICS constant that was no longer used
  • Removed hass_config property from Hassette class (configuration is now flattened)
  • Cleaned up unused imports and redundant code
  • ResourceSync
  • stop method on Resource
  • __init__ on Service that was the same as the parent class

Internal

  • Simplified configuration test files to use new flattened structure
  • Updated all import statements throughout the codebase to reflect new module structure
  • Simplified app handler path resolution by using full_path property directly
  • Updated test configuration and example files to match new config structure
  • Enhanced state model discriminator logic for better type resolution
  • Consolidated configuration access patterns for cleaner code

[0.3.3] - 2025-09-07

Fixed

  • Filter pydantic args correctly in get_app_config_class utility function so we don't attempt to use typing.TypeVar as a config class.

[0.3.2] - 2025-09-07

Fixed

  • Removed incorrect __init__ override in AppSync that was causing issues with app instantiation

[0.3.1] - 2025-09-07

Fixed

  • Fixed timestamp conversion return types in InputDateTimeState attributes
  • Removed custom attributes from input number states
  • Get AppSync working using anyio.to_thread and hassette.loop.create_task to ensure we're on the right event loop

Internal

  • Consolidated input entity states into unified input.py module
  • BinarySensorState now inherits from BoolBaseState
  • Fixed inheritance issues in SceneState, ZoneState, and NumberState
  • Update health service to use a web.AppKey instead of a string

Tests

  • get tests against HA instance working in Github Actions
  • updated tests for fixed synchronous app handling

[0.3.0] - 2025-09-04

Changed

  • Update exports to remove long lists of states, events, and predicates
  • Still export StateChangeEvent
  • Other exports are now under states, events, predicates exports
  • E.g. from hassette import AttrChanged becomes from hassette import predicates and predicates.AttrChanged

[0.2.1] - 2025-09-04

Added

  • New examples directory with comprehensive automation examples
  • Battery monitoring example app with sync/async variants
  • Presence detection example with complex scene management
  • Sensor notification example
  • Example hassette.toml configuration file
  • New on_hassette_service_started event handler
  • Additional sync API methods for better synchronous app support

Changed

  • Improved README with comprehensive documentation and examples
  • Updated pyproject.toml with better PyPI metadata and project URLs
  • Enhanced notify service examples and API calls
  • Updated roadmap with current development priorities

Fixed

  • Fixed notify service call examples in battery and presence apps
  • Fixed HassetteServiceEvent annotation

[0.2.0] - 2025-09-04

Added

  • Full typing support for Home Assistant entities and events
  • Custom scheduler replacing APScheduler dependency
  • Comprehensive state model system with typed attributes
  • Event bus with powerful filtering capabilities
  • Testing utilities and mock server support

Changed

  • BREAKING: Significant changes to state/sensor structure for better type safety
  • Made sensor attributes required and always present
  • Simplified state management by moving simple states to dedicated module
  • Reduced complexity in state handling while maintaining full functionality
  • Updated authentication and HTTP handling

Fixed

  • API parity between sync and async methods
  • Sensor attribute handling and device class support
  • Configuration scope and core initialization

Internal

  • Moved sensor literals into constants module
  • Reorganized state models for better maintainability
  • Added comprehensive test coverage for API parity
  • Improved development tooling and testing setup

[0.1.1] - 2025-09-02

Added

  • Initial public release of Hassette framework
  • Basic async-first Home Assistant automation support
  • Type-safe entity and event handling
  • TOML-based configuration system
  • Pydantic model validation for app configs

Features

  • Event-driven architecture with asyncio
  • Home Assistant WebSocket API integration
  • Structured logging with coloredlogs
  • Scheduler for cron and interval-based tasks
  • App lifecycle management (initialize/shutdown)