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 aFatalError, 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_specare replaced by arequiredclass attribute that every concreteServicesubclass must declare: an app-ownedServicemust setrequired = False, and a required service under any parent other thanHassetteraises atadd_child.Resource.restart()is removed.ResourceStatus.EXHAUSTED_DEAD,ResourceStatus.EXHAUSTED_COOLINGandretry_atare removed fromhassette,hassette-wireandhassette-client, and session rows no longer recorderror_typeorerror_traceback. (#3524) - Shutdown timeouts are per hook and per stage —
app_shutdown_timeout_seconds,resource_shutdown_timeout_secondsandsync_executor_shutdown_timeout_secondsare 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_secondsnow 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 (Dockerstop_grace_period, systemdTimeoutStopSec); 45 can kill Hassette before the shutdown snapshot is written. (#3403) hassette runhard-exits if work outlives shutdown — after shutdown,hassette runcancels 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 bycreate_core(),serve()andrun(). See "How shutdown is bounded" in the operating docs. (#3587)TeardownReportis replaced byShutdownOutcome—TeardownReportandTeardownCauseare replaced byShutdownOutcomeandShutdownResult(hassette.resources).ShutdownResultis rankedDONE<FAILED<TIMED_OUT<INCOMPLETE, and an outcome carries the names of tasks still running plus one reason.Resource.teardown_reportis nowResource.shutdown_outcome, andRestartRefusedError.reportis nowRestartRefusedError.outcome. Read.outcomeinstead of parsing the error message.is_restart_safenow 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 checkresultinstead.Hassette.force_terminated_workandrecord_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.statusis removed, and the requiredActionResponse.outcomeshas one entry per targeted instance:instance_index,instance_name,result(succeeded,failedorrefused),needs_restartandreason. A 409needs_restartmeans an instance was already flagged as needing a Hassette restart, so the action refused it (its siblings were still acted on). A 500action_failednow also covers a failed stop and an action that leaves an instance needing a restart. Both error bodies carryoutcomes.hassette-clientrequires a server at API schema 4; if you retry on 409, catch the newNeedsRestartErrorbefore a genericConflictError.hassette app start|stop|reload --jsonprintsoutcomesand no longer printsstatus. (#3575) - Removed settings — each of these is now an unknown-key
ConfigErrorat load; delete them fromhassette.tomland fromHASSETTE__*environment variables:strict_lifecycle. Startup registry validation always logs warnings. (#3483)[hassette.websocket]authentication_timeout_seconds,response_timeout_seconds,total_timeout_secondsandcleanup_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)
InvalidLifecycleTransitionErroris removed — assigning aResourceStatusnow always applies (logged at DEBUG) instead of being checked against a transition table. Drop anyexcept InvalidLifecycleTransitionErrorhandlers. (#3480)
For App Authors: You Can Now Delete
- A raised
total_shutdown_timeout_seconds, oron_shutdownhooks kept artificially short, so later apps weren't cancelled at exit — every app's shutdown hooks now get their fullshutdown_timeout_secondshowever many apps there are. Re-check any explicit total against the new foundation stage (see Breaking Changes). (#3403) - A raised
[hassette.lifecycle] startup_timeout_secondsto let a long database migration finish — the database startup phase now getsstartup_timeout_secondsplusdatabase.migration_timeout_secondson its own. (#3505) - A raised
database.max_consecutive_heartbeat_failuresto stopDatabaseServicerestarting during long retention passes — a heartbeat failure no longer counts while writes are still completing. (#3457) - Warning filters or
IGNOREentries for one-offHassetteBlockingIOWarningloop 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'sstartup_timeout_secondscovers 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
Restartcolumn inhassette app, in ahassette statustable, and asneeds_restartonAppInstance,AppSummary.needs_restart_countand theapp_status_changedevent. 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(default2) hides Tier 1 findings with fewer events from the findings list. Set it to1for 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_atandjitterin 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_atis 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_secondsdefault 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_secondsat 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 onestackand oneexceptionkey. Stored log rows keep the traceback separately, so the dashboard log drawer's exception panel shows it.hassette logandhassette executiontables show the exception'sType: messageline 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 underpython -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_urlwithuser:password@userinfo when a Hassette token is also configured, instead of silently dropping the token and failing authentication. (#3430) - The Docker
latesttag only moves for the highest stable release, so a patch to an older release line no longer moveslatestbackwards. (#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 whenon_initializefails. (#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,Topicin 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_startedordering, 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_nameis required and unique — a configured app instance withoutinstance_namenow 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 likeMyApp.0are gone, so cache keys, eventsourcecomparisons and logger names built from them change.AppTestHarnessnow requires aconfigthat setsinstance_name. Addinstance_nameto 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 fromhassette.apps.<instance_name>.Lifecycle, followlogging.app_handlerinstead of the app'slog_level, and are markedframeworkrather thanapp. Setlogging.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_tieron log records is nowauthor—LogEntry.source_tier(wire,GET /api/logs/recent,GET /api/executions/{execution_id}) is nowLogEntry.author,"app"or"framework". Thesource_tierquery parameter on the log routes is replaced byauthor(all,apporframework); a straysource_tieris ignored and the default view applies.HassetteClient.get_recent_logs(source_tier=...)is nowauthor=...,get_execution_logsacceptsauthor, and the client requires API schema 3.hassette log --source-tieris now--author, andhassette executionaccepts--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. Passauthor=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.autodetectandapps.exclude_dirsare gone, soHASSETTE__APPS__AUTODETECTandHASSETTE__APPS__EXCLUDE_DIRSnow 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"]. Theauto_loadedfield in app API responses is deprecated and alwaysfalse; it will be removed in a future release (#3161). (#3158)
For App Authors: You Can Now Delete
- Raising
logging.bus_service,logging.scheduler_serviceand similar globally to debug one app — setframework_log_levelon that app's config to see Hassette's lines about just that app. (#3178) - Workarounds for
get_state_value()andget_attribute()underAppTestHarness— code that was rewritten to callget_state(), or tests that hand-mocked those two calls, can go back to the natural calls, async or viaself.api.sync. (#3132) - Hand-mocked
get_state_raw()andget_states_raw()in tests — seed the entities and read them throughRecordingApi. (#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=valuetail 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 anHH:MM:SStimestamp and no level padding. The Home Assistant add-on uses it so the Supervisor Log tab reads on a phone.autonever picks it. (#3203) - New
framework_log_levelonAppConfigsets the level for every Hassette logger one app instance owns: its lifecycle logger and its Bus, Scheduler, Api, StateManager and TaskBucket loggers.log_levelstill governs only the app's own logger. Unset, nothing changes. Shared services such asCommandExecutorhave no owning app and keep theirlogging.*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
Appsubclass 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. Setapps.warn_unconfigured = falseto 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
RecordingApisupportsget_state_value()andget_attribute()(with dot paths, returningMISSING_VALUEfor absent attributes), andget_state_raw()andget_states_raw(), which return deep copies of the seeded state. All raiseEntityNotFoundErrorfor unseeded entities. (#3132, #3130)
State Models
self.states.scheduleandself.states.groupresolve to typed models (ScheduleStatewithnext_event, andGroupStatefor legacygroup.*entities) instead ofBaseState. (#3131)
CLI
- Status and health values in the
hassette status,app,app activityanddashboardtables are colored on a terminal and plain when piped;--jsonis unaffected. (#3126, #3150) hassette logtables 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_servicefailureswait_for_acksurfaces (a nonexistent entity ID is only logged by Home Assistant), describedget_state().valueas a converted, domain-typed value, fixed theget_attributedot-path example, and showed the fullAppSynccover 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
fpathsetup so shell completion loads. (#3183)
0.59.0 (2026-10-19)
Breaking Changes
roleis gone from the web API's service status —GET /api/healthno longer includesservices[].role,hassette_wire.ResourceRoleandOpenResourceRoleare removed, andhassette.types.ResourceRoledrops its unusedCORE,BASEandUNKNOWNmembers. Clients built before this release requireroleand can't parse/api/health: upgradehassette-clientto 0.59.0, and update thehass-hassetteintegration 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()andAfter()raiseValueErrorfor a total delay under 1 second, andrun_every()andEvery()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, orawait asyncio.sleep()inside a handler for a shorter pause. Fractional jitter is still allowed. (#3077) Resource.cleanup()takes no arguments — thetimeoutparameter onResource.cleanup()and theAppandDatabaseServiceoverrides is removed. Shutdown already bounds cleanup withcleanup_timeout, so it did nothing. A customResourceorServiceoverride should becomeasync def cleanup(self) -> Nonecallingawait super().cleanup(). Passing the old argument, as insuper().cleanup(timeout), raisesTypeError, which shutdown records as a failed cleanup. The unusedLifecycleMixin.initializingandLifecycleMixin.taskproperties are also removed. (#3117)
For App Authors: You Can Now Delete
- Custom state classes for
wake_word,infrared,radio_frequency,alertortagentities — these domains now have typed models (self.states.tag,self.states.alert, and so on) instead of falling back toBaseState. (#3103) - Raising
logging.web_apitoWARNINGjust to silence per-request access lines — access lines are now logged only whenlogging.web_apiisDEBUG, so the default no longer fills the log with healthcheck and integration polling. (#3050)
Bus, Scheduler & Apps
on_state_changeandon_attribute_changelog 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_frequencyandtag(timestamp state of the last event, withtag_idandlast_scanned_by_device_idontag) andalert(string state, plus analertentity wrapper). (#3103) - Apps can declare an optional one-line
description(up to 200 characters) in their manifest. (#3116)
Web API & Telemetry
GET /api/appsandGET /api/apps/{app_key}return each app'sdescription, ornullwhen 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_apiisDEBUG. Startup and error lines still followlogging.web_apias 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_urlis 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'shttp://supervisor/core) now works. Abase_urlwith a leftover page path, such as a browser URL likehttp://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. Seebase_urlPaths in the upgrading guide. (#2974) - Config settings removed or given one meaning — (#2784, #2783)
apps.extend_exclude_dirsis removed. Move its entries toapps.exclude_dirs, which now adds to the built-in exclusions instead of replacing them.logging.all_eventsis removed. Replaceall_events = truewithall_hass_events = trueandall_hassette_events = true. Events are now classed by payload type, soall_hassette_eventsalso covers topics apps publish withBus.emit.- A
logging.log_persistence_levelstricter thanlogging.log_levelis now a configuration error instead of being silently clamped. Unset, it defaults tolog_level. lifecycle.registration_await_timeoutandweb_api.job_history_sizeare removed. Nothing read them; job history is governed bydatabase.retention_daysanddatabase.max_size_mb.- App entries must use
filenameandclass_name. Thefile_name,class,moduleandmodule_namespellings 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_daysalone fails validation —database.retention_days(was 7),database.framework_retention_days(was 1) andlogging.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 belowretention_days, so a config that sets onlyretention_days = 14now fails. Lower all three together. (#2814) - Legacy environment variable spellings are no longer read —
LOG_LEVELandHASSETTE_LOG_LEVELbecomeHASSETTE__LOGGING__LOG_LEVEL(the old names never set the level past bootstrap anyway),HASSETTE_CONFIG_DIRbecomesHASSETTE__CONFIG_DIR, andHASSETTE_DATA_DIRbecomesHASSETTE__DATA_DIR. (#2777) - REST failures other than 404 raise
HttpRequestError— REST-backedApimethods raised a bareaiohttp.ClientResponseErrorfor statuses such as a 503 during an HA restart, which apps catchingHassetteErrormissed. CatchHttpRequestError(aHassetteErrorcarryingstatus,message,methodandurl) instead. 404s still raiseEntityNotFoundError, and retries are unchanged. (#2939) FalseySentinelis a closedEnum—MISSING_VALUE,NOT_PROVIDEDandANY_VALUEbehave as before (ischecks, falsiness,isinstance, pickling), butFalseySentinel("NAME")now raisesValueErrorand subclassing raisesTypeError. Define your ownEnumwith a__bool__returningFalseif you need a custom falsey sentinel. Pyright can now narrowischecks against individual members. (#2834)- Hassette requires
whenever0.11 (>=0.11,<0.12) — apps that usewheneverdirectly must move off the APIs 0.11 deprecates or removes: the system-timezone helpers become theSYSTEM_TZspellings,timestamp_nanos()becomestimestamp(unit="nanosecond"),ZonedDateTime.tzbecomestz_id, andTime.py_time/Date.py_datebecometo_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)) — writerun_in(func, minutes=5)orhours=. (#2808) - Handlers that store the previous reading to detect a threshold crossing — use
changed=C.CrossedAbove(threshold)orC.CrossedBelow(threshold), which fire only on the transition. (#2943) - A separate
on_attribute_changelistener that reschedules anEntityTimejob becauseattribute=ignored attribute-only changes —EntityTime(..., attribute=...)now follows the attribute itself. (#2978) - Reaching into
AppTestHarness._harnessto send an event thesimulate_*helpers don't build — useharness.simulate_event(event). (#3006) - A hand-rolled
asyncio.Semaphorearoundtask_bucket.spawn()— passmax_tasks=to theTaskBucket. (#2833) cast()or a pyright ignore on acall_servicetargetthat mixes one ID with a list —targetis typedMapping[str, str | list[str]], so{"entity_id": "light.kitchen", "area_id": ["hall", "den"]}type-checks. (#2832)- Deleting or moving
.venv/node_modulesout 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 aHEALTHCHECKon/api/health/live. It connects to HA through the Supervisor proxy with no token pasting, seeds an empty/configwith a commentedhassette.tomland an example app, and pins the settings the add-on owns sohassette.tomlcan'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_packagesoption 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_prehook, and a restore puts it back on start. The newdatabase.shutdown_snapshot_pathsetting 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-hassetteintegration 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_filesetting chooses where the server reads and writes its generated token (default<data_dir>/.web_api_token). The add-on points it at/configso the token is reachable from File Editor or Samba, and the CLI reads the same path. (#3001)
Bus, Scheduler & Apps
C.CrossedAbove(threshold)andC.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_intakes keyword-onlyminutes=andhours=alongside its positionaldelayin seconds; the parts add up. (#2808)EntityTimewithattribute=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'snext_*attributes, a next-alarm sensor) never fires; the docs say so and suggest a negative offset or aninput_datetime. (#2978)TaskBuckettakes an optionalmax_tasksthat bounds how many spawned coroutines run at once.spawn()stays synchronous. (#2833)- New
AnyEventalias (Event[EventPayload[Any]]), exported fromhassette.events, typesevent.payloadin handlers that accept any event. A handler annotatedAnyEvent | Nonegets the event injected likeEvent | 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)
StateComparisonandAttrComparisonraiseTypeErrorwhen passed a condition class such asIncreasedinstead 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_urlerrors, a rejected token, dependency-injection errors (with a docs link) and the state-registry domain errors. (#2809) - State conversion that falls back to
BaseStatenow 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 plainBaseState. (#2912)
Telemetry & Retention
- Retention cleanup deletes old
sessionsrows 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/recenttakes an optionalinstance_indexto 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 runshows 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_proxieshostname 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<2now 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
--jsonmode, a command against a non-loopback target prints theTarget: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-hassetteintegration 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_lightsexample and the debounce recipe no longer turn a light off when motion returns; they useduration=instead ofdebounce=. (#2797, #2785) - Scheduling relative to sunrise and sunset with
EntityTime. (#2957) - Which state-conversion paths fall back to
BaseStateand which raise. (#2937) call_service'starget=and targeting keys in**dataare equivalent;target=is preferred. (#2827)- Terms defined on first use in the API methods, Apps and Task Bucket pages, and corrected
hassette appsample 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_initializemust handlewebsocket_connected(orhomeassistant_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.CouldNotFindHomeAssistantErrorand theTEST_EARLY_DROP_*testing constants are removed, andInvalidAuthErroris now aHassetteErrorrather than aFatalError. (#2743) - Unknown settings fail startup, and several config paths and names moved — see the groups below. (#2641)
- Unknown settings: an unknown
HASSETTE__*environment variable orhassette.tomlkey stops startup with exit code 78. The error names the key and suggests the closest real setting.HASSETTE__LOG_LEVELis nowHASSETTE__LOGGING__LOG_LEVEL. - Config locations:
config_dirdrives file lookup and can't be set inhassette.tomlor.env; pass--config-diror setHASSETTE__CONFIG_DIRin 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. Theconfig_fileandenv_filesettings are retired; use the--config-fileand--env-fileCLI flags. - App config: an
AppConfigenv_prefixthat starts withhassette__is now an unknown-key error because that namespace is reserved. Rename it to something likeMYAPP_.AppConfigreads the same.envfiles as the running Hassette config instead of the fixed/config/.env,./.envand./config/.envlist; setenv_filein the app'smodel_configto read a specific file. App config overrides throughHASSETTE__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 runlogs to stdout; update log collection that read stderr. - Docker: apps moved from
/appsto/config/appsand the image no longer setsHASSETTE__APP_DIR. Move./appsto./config/appsand drop the./apps:/appsvolume, or keep the mount and setHASSETTE__APPS__DIRECTORY=/apps. Thedocker_start.shvariables were renamed with no fallback:HASSETTE__INSTALL_DEPS,HASSETTE__PROJECT_DIRandHASSETTE__PRUNE_UV_CACHEare nowHASSETTE_DOCKER_INSTALL_DEPS,HASSETTE_DOCKER_PROJECT_DIRandHASSETTE_DOCKER_PRUNE_UV_CACHE. run_app_precheckanddisable_state_proxy_pollingremoved — both now fail startup as unknown keys. The app precheck always runs; setallow_startup_if_app_precheck_fails = trueto start despite a failure. The state cache poll always runs; tune it withstate_proxy_poll_interval_seconds. (#2762)scheduler.min_delay_seconds,scheduler.max_delay_secondsandscheduler.default_delay_secondsremoved — they only tuned an internal sleep clamp that made jobs fire late, and have no replacement. Delete them fromhassette.tomlandHASSETTE__SCHEDULER__*environment variables, or startup fails with the unknown-key error above.behind_schedule_threshold_secondsis unchanged. (#2726)GET /api/logs/recentvalidatessource_tieras a case-sensitive enum —app | framework | all, defaulting toall, matching the telemetry endpoints. Values that used to be lowercased server-side, such assource_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 againstlast_changedor the clock inside a handler orshould_run(). Usewhere=P.EventEntityFresh(max_age=...). (#2631) changed_to=/changed_from=callables or numeric string literals written becauseTrueand20never matched — a lambda likelambda v: v == "on"for a boolean, or"20.0"for a number.changed_to=Truenow matches"on"andchanged_to=20matches"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 typedtarget_temperature,target_temperature_high,target_temperature_lowand, on water heaters,target_temperature_stepfields. (#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 inAUTH_FAILEDwithout 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_urlhint. (#2743) - The WebSocket's
max_msg_sizeis raised to 16 MiB so a largeget_statespayload no longer triggers a close loop. (#2743) - A clean shutdown no longer logs
task ws:recv crashedat ERROR (#2759), andrequest_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 thechanged_from=/changed_to=kwargs.C.Comparison("==", True)now agrees withchanged_to=True. Use the newC.Exact(value)when you need a strict match against the raw value. (#2684) P.EventEntityFresh(max_age=...)drops stale replays ofevent.*entities, such as buttons and remotes, that HA re-broadcasts after a restart. It compares the entity's timestamp state with itslast_changed, never reads the current clock, and fails open when either value is missing or unparseable. (#2631)wait_forcancellation 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 pendingwait_forcalls reach 80% oflifecycle.max_concurrent_dispatches, andhassette statusandGET /api/healthreportpending_wait_for_count. (#2744, #2723, #2725)- Removing a listener through the sync facade (
bus.sync.remove_listener/remove_all_listeners) now cancels a pendingwait_foron 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)
Afteracceptshours=, matchingEvery. Combiningtimedelta=withseconds=/minutes=/hours=now raisesValueError; before, the unit arguments were silently dropped. (#2648)- A
single/queued/restartjob that replaced itself from its own callback withif_exists="replace"used to hang forever. It now works for async and sync callbacks. (#2650) Scheduler.remove_all_jobs()no longer raisesTaskBucketSealedErrorduring a forced shutdown. The removal methods are typed asasyncio.Future[None]rather thanasyncio.Task; awaiting callers are unaffected. (#2660)- A handler or job that raises
TimeoutErroritself, such as await_fortiming out or anaiohttprequest timeout, is recorded as an error with a traceback instead oftimed_out.timed_outnow 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 alwaysNonebefore, with the values only inattributes.extras. The values no longer appear inextras, so code reading them there must switch to the typed fields. (#2635) - A typed
AppConfigsubclass now warns when an app'shassette.tomlconfig has keys it doesn't declare, naming the app and suggesting the closest declared field. (#2585) display_nameset on an app inhassette.tomlis honored instead of silently dropped. (#2722)- Missing public types are exported from their package
__all__lists:TypedStateChangeEventandTypedStateChangePayloadfromhassette.eventsandhassette,FalseySentinelfromhassette.const,BlockingIOBehaviorandForgottenAwaitBehaviorfromhassette.types, andBlockingIODetectionConfigfromhassette.config.HassetteNotInitializedErrornow subclassesHassetteError, and still subclassesRuntimeError. (#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 ashttps://example.com/hassette/. The base path comes fromX-Ingress-PathorX-Forwarded-Prefix, honored only from atrusted_proxiespeer 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,stoporreloadrequest for an app that already has an action running gets an immediate 409action_in_progressinstead 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 --instanceis hidden from--helpand completion. (#2645) hassette statusshowsapp_countas "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_mbonly because of free pages or an untruncated WAL, andincremental_vacuumreclaimed 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'sclear()runs a fullVACUUM. (#2751) - Retention and size-failsafe cleanup resume when
DatabaseServicerestarts 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_pollexecution. (#2623)
Documentation
- New guide for controlling Hassette apps from Home Assistant with the
hass-hassetteintegration. (#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 withuv 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,CandP, the acceptedchanged_from/changed_toforms 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
cancelledandtimed_outruns that makehassette job'sTotalexceedOK + Fail + Skipped. The lifecycle internals page states the correctServiceWatchershutdown order. (#2652, #2657, #2620)
0.56.0 (2026-10-08)
Breaking Changes
App code
AppConfig.app_keyremoved — the field was never populated, soself.app_config.app_keywas always"". Read the app's key withself.app_keyinstead. Anapp_keyleft under an app's config inhassette.tomlstill loads, but is now an ignored extra field. (#2470)ResourceStatusandExecutionStatusimport paths moved —hassette.types.ResourceStatus,hassette.types.enums.ResourceStatusandhassette.types.types.ExecutionStatusno longer resolve. Import them from the package root:from hassette import ResourceStatus, ExecutionStatus.ExecutionModeandBackpressurePolicyare also exported from the root. TheLOG_LEVEL_TYPEalias is gone; usehassette_wire.LogLevel. (#2449)AppConfig.log_leveldefaults toNone— code that readsself.app_config.log_leveldirectly now getsNonefor apps that don't set it, instead of"INFO". Useself.config_log_levelfor 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
/apiis nowapplication/problem+jsonwithtype,title,status,detailand a stablecode(see the new API errors page). Status codes anddetailstrings are unchanged. Validation 422s now carry a stringdetailsummary instead of a list, and with the SPA served, a non-GET request to an unknown/apipath 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_unavailableproblem body instead of a 503 with an empty or default body./api/health/readyand/api/telemetry/statusare unchanged. (#2491) - Apps endpoints reshaped — update callers as follows (#2558):
GET /api/apps/manifests→GET /api/apps; the list fieldmanifests→appsGET /api/apps/{app_key}/manifest→GET /api/apps/{app_key}- The old
GET /api/appslive-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 --jsonandhassette dashboard --jsonrows are also{app, activity};recent_invocations_1hleaves the app summary- Activity that wasn't computed is now
nullper part (stats,activity_buckets,last_error,blocking_event_count) instead of zeros — handlenull - A non-finite
since(nan,inf) now returns 422 - One
AppHealthmodel —GET /api/telemetry/app/{app_key}/healthreturnsAppHealth, with averages renamed tohandler_avg_duration_ms/job_avg_duration_ms(nullwhen nothing of that kind ran). Grid rows nesterror_rate,error_rate_class,health_statusandlast_activity_tsunderhealthand dropavg_duration_ms.hassette app health --jsonrenameshandler_avg_duration/job_avg_durationto*_ms. Health now counts executions of handlers and jobs removed mid-window. (#2520) - Typed vocabularies —
role,schedule_status,schedule_status_reasonand app action fields are now enums in the OpenAPI and WebSocket schemas.ServiceInfo.roleandJobSummary.schedule_statusare now required. (#2527) hassette_wiremodel 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→ListenerSummaryAppManifestResponse→AppSummary,AppManifestListResponse→AppListResponse,DashboardAppGrid*→AppGrid*,ManifestStatus→AppStatusAppHealthResponse→AppHealth;AppStatusResponseis removed
For App Authors: You Can Now Delete
await super().before_initialize()added only to keepself.cacheworking — the cache is now initialized by the framework before your hook runs, so an override no longer needs thesuper()call. (#2572)cast()aroundself.cache.get(key, default=...)—get()with a non-Nonedefault is now typed as the default's type, socast(list[float], ...)wrappers andor 0fallbacks can go. (#2528)- Per-app
log_levelset only to match[hassette.logging] apps— apps without their ownlog_levelnow inherit the global apps level. (#2571) deep_detection_enabled = trueset alongsideallow_deep_detection_in_prod = true— the allow flag now enables Tier 2 blocking-IO detection in production on its own. (#2472)try/exceptinside predicates onduration=listeners to catch your own errors — a raising duration-hold predicate is now recorded as an error execution and routed to the listener'son_errorhandler, 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-clientnow shipsHassetteClient: typed async methods over anaiohttp.ClientSessionyou own, returninghassette_wiremodels. Every network or server failure raises aHassetteClientErrorsubclass (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_versionin its health payload, andcheck_server_version()raisesUnsupportedServerVersionErroragainst a server too old for the client (#2596) - Client responses parse leniently: an enum or
Literalvalue a newer server adds arrives asUnknownValue(astrsubclass that keeps the raw value) instead of failing validation. Opt in on your own parses withhassette_wire.LENIENT_CONTEXT. (#2502) hassette-wirenow 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.tomloverlay for per-machine settings you don't want to commit (a devbase_url, a debug log level). Each configured TOML file gets its own<name>.local.tomlsibling, 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 = truesilences thecache_keycollision 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 blockingcommand. 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 dashboardtables 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 activityandhassette status(App Key→App,Invoc/1h→Invocations/1h,Errs→Errors) (#2544)
Testing
AppTestHarness.simulate_*()helpers return the typed event they dispatched (for exampleRawStateChangeEventfromsimulate_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)withoutwait_for_ack, and helpercreate/update/deletenow send exactly once and raiseResponseTimeoutError, orResponseLostErroron disconnect. Both subclass the newOutcomeUnknownError: 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
HassetteNotInitializedErroruntil 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_levelnow inherit[hassette.logging] appsinstead of always usingINFO(#2571) self.cacheworks when an app overridesbefore_initializewithout callingsuper(), instead of raisingAsyncCache is not initialized(#2572)allow_deep_detection_in_prod = trueenables Tier 2 blocking-IO detection on its own. Note: an explicitdeep_detection_enabled = truein production now turns Tier 2 on too;deep_detection_enabled = falsealways wins. (#2472)- A predicate that raises on a
duration=listener is now recorded as an error execution and sent toon_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 escapedata_diror alias another app's (#2579) - WebSocket send and dispatch-failure logs and
FailedMessageErrormessages no longer include payload values, which could contain secrets such as a password-modeinput_textvalue. The payload is still onFailedMessageError.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 asx's type instead ofT | 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 = falsebehavior, and config file discovery (everyhassette.tomlis merged) (#2465) - The bus methods page states correctly that omitting
name=raisesTypeError, and that every handler-taking method acceptson_error(#2441) HelperClient.update()anddelete()take the helper's storage id ("vacation_mode"), not itsentity_id; the docs and docstrings now say so (#2450)- Blocking-IO guidance points to
self.task_bucket.run_in_threadinstead ofasyncio.to_thread(#2523) /api/healthreportsokonly once the WebSocket is connected and app bootstrap is released (#2471)- Fixed the
A.get_pathexample 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_filesis 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'stimeout_secondsparameter now defaults to aNOT_PROVIDEDsentinel instead ofNone. Previously,Nonemeant "use the config default" andfloat("inf")meant "block forever" internally. Now, omitting the argument (or passingNOT_PROVIDED) means "use the config default," andNonemeans "block forever" (matchingconcurrent.futures.Future.result(timeout=None)).run_syncis public, documented API — any caller that explicitly passedtimeout_seconds=Noneto 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) Topicis now exported from the top-level package:from hassette import Topic(#2360)Api.call_service()gainswait_for_ack=True, which waits for Home Assistant to confirm the call and raises on failure. It works for services that return no response, wherereturn_response=Truewould be rejected. (#2252)- Generated entity methods for services that return data now return the
ServiceResponseinstead ofNone:weather.get_forecasts(),todo.get_items(),media_player.browse_media(), andmedia_player.search_media()(#2237)
Telemetry & Database
- New
database.framework_retention_days(default1) 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 beforeretention_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, anddatabase.framework_record_sample_rate. Dropped records are counted in the newdropped_filteredfield 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(default60) 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_*, andmax_consecutive_heartbeat_failuressettings are now honored. Previously they were accepted but ignored in favor of hardcoded values. (#2296)
Logging
- New
logging.extra_loggerssetting sends loggers outside thehassette.*namespace (for example, your app's ownmy_app.notifylogger 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_levelis now clamped tolog_level, so the dashboard never hides records that are actively being logged (#2411) web_api.log_buffer_sizehas 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
401fromhassette statusand 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 jobshows aSkippedcolumn, so a predicate-gated job whose predicate never passes no longer reads asTotal 68 / OK 0 / Fail 0(#1958)
Packaging
hassettenow depends on two new packages released in lockstep with it,hassette-wireandhassette-client. Both are currently empty placeholders for an upcoming split of framework internals. No imports change. (#2422)- The Docker image ships
httpx2/httpcore22.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 restartwarnings 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}/startand/reloadnow return 500 with the error message when the instance ends upFAILED, instead of reporting success with 202 (#2370)HelperClient.increment(),decrement(), andreset()for counters no longer fail every time withservice_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_shutdownhook) 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=Truelisteners 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 raisesAttributeError. (#2251)predicate_descriptionin 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
MotionLightsconfigs throughout, consolidate the seed-vs-simulate guidance into one place, and document thatfreeze_timetakes 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, andVacuumAttributes.supports_batteryhave 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 pairedsensor.*_batteryentity), 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_positiongain an optionalspeedkwarg, andWaterHeaterEntityStateAttributegainsTARGET_TEMPERATUREas an alias ofTEMPERATURE(#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_utilsno longer exists. Public (Tier 1) symbols move tohassette.testing; everything else moves totests.support(internal-only, not shipped in the wheel — not usable outside this repo). (#1881) #### Public API — update the import path onlyApiCall,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_statenow import fromhassette.testinginstead ofhassette.test_utils.
Public API — newly promoted to Tier 1 (previously internal-only)
EventCapture,HassetteHarness,build_harness,make_full_state_change_event,wait_forare now part of the supportedhassette.testingsurface.
Removed from the public surface
make_mock_hassettewas previously inhassette.test_utils.__all__; it is now internal-only (tests.support.mock_hassette, not shipped in the wheel). External consumers using it should build their ownHassettestub, 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 athassette.test_utils.*before; it now lives attests.support.*inside this repository only.
SensorAttributesno longer declaresnative_value,native_unit_of_measurement, orsuggested_display_precision(they duplicated the baseState.value/unit_of_measurementfields and are removed for thesensordomain only).StateRegistry.registerandregister_state_converterdrop their unuseddevice_classkeyword parameter, andStateKeydrops itsdevice_classfield (kept as a one-field frozen dataclass withdomainonly — its export names andisinstancechecks are unchanged). (#1549) #### What changes for callers- Code reading
sensor_state.attributes.native_value,.native_unit_of_measurement, or.suggested_display_precisionmust switch tosensor_state.valueandsensor_state.attributes.unit_of_measurement— those fields already carried the same data. - Code calling
StateRegistry.register(..., device_class=...),StateRegistry.resolve(..., device_class=...), orregister_state_converter(..., device_class=...)must drop thedevice_classkeyword — it was never populated or read by any production call site. - Code constructing
StateKey(domain=..., device_class=...)directly must dropdevice_class—StateKeyis nowStateKey(domain)only.
- Code reading
Features
- New device-class-specific sensor state subtypes, so a sensor's
.attributesare 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_atdisplay for non-jittered jobs, a wrongapp_activitydefault time window, unhelpful CLI error framing for malformed 2xx responses, duplicate app-init error logs, log-table key collisions onseq: 0, and query-param parsing edge cases (#1872) - Job/handler status reporting is now exhaustive over all statuses, fixing gaps where
degraded/skippedstates weren't handled (#1671) hassette runnow 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
warningsmodule 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: ignorecomments 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 (exceptGET /api/health/live,GET /api/health/ready, andPOST /api/auth/session) returns 401 without a valid bearer token, session cookie, ortrusted_proxiesmatch. #### 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 fromdocker logs(or the token file) and paste it into the new login view, or setHASSETTE__WEB_API__AUTH_TOKENexplicitly. - CLI commands (
hassette status,hassette app, etc.) now attach the token automatically from the token file orHASSETTE__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_proxiesto the gateway's address instead of managing a token. cors_originsmay no longer contain"*"— config load now rejects it, sinceallow_credentials=Trueis hardcoded.GET /api/docsand/api/openapi.jsonnow require a credential like every other/api/*route.- Setting
auth_enabled=falserequireshostto be a loopback address (127.0.0.1/localhost) — hassette refuses to start otherwise. (#1521)
- On first start after upgrading, hassette generates a token, logs it
once, and persists it to
- 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_tokenfile) to a non-loopback target. Previously, pointing the CLI at a remote host viaHASSETTE__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 withtrusted_proxiesconfigured for it, keeps working with no credential sent at all. Configure a remote-target credential via--token-file,cli.token_file, orHASSETTE__CLI__AUTH_TOKENinstead. Separately,hassette run's--base-urland--urlflags are removed — use--ha-url(same-ushort flag). (#1529)
Web API
- Session cookie auth for the dashboard, pre-accept WebSocket checks — browsers exchange the token once via
POST /api/auth/sessionfor anHttpOnlysession cookie, since the native WebSocket API can't set custom headers. The WebSocket handler now runs its credential check beforeaccept(), 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-sslflags — 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 fromweb_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/sessionby 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_proxiespeer, hassette previously never checked the token at all, silently authenticating the request even with a wrong token. A presentedAuthorizationheader 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
SecretStrvalue inside a container field (list[SecretStr],dict[str, SecretStr], aSecretStrinside a nested model) was previously returned in plaintext byGET /api/configand the per-app config endpoint. (#1534) - Codegen renders external strings as safe literals — Home Assistant constants and
services.yamldescriptions 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—ScheduledJobis renamed toJob, the single public runtime type returned by bothschedule()and the newregister().ScheduledJob.cancel()→Job.remove(),Scheduler.cancel_job()→Scheduler.remove_job(),Scheduler.cancel_group()→Scheduler.remove_group(). The persistedcancelled_atcolumns are renamed toremoved_at(migration 012 handles this automatically).next_run == nullno longer implies a job has finished — read the newschedule_statusfield (scheduled,waiting,completed, ormanual) instead. No compatibility aliases are kept. (#1500) - Health endpoint reports
degradedduring app bootstrap —GET /api/health/readynow returns 503 during the window between WebSocket connection and initial state-sync completion, where it previously returned 200.SystemStatusResponsegained abootstrap_releasedfield, and a synthesized boot issue ("Apps pending on Home Assistant") appears while bootstrap is pending. Monitoring integrations polling/api/health/readywill see the new 503 window on upgrade. (#1495) SystemStatusResponse.log_records_droppedsplit into two fields — replaced bylog_queue_dropsanddb_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 byget_log_queue_drops()andget_db_write_queue_drops(). (#1469)ListenerWithSummary.entity_idrenamed totarget— the web API response field (onGET /api/bus/listenersandGET /api/telemetry/app/{key}/listeners) now populates event-type listeners with a topic-derived name instead ofnull. (#1442)only_apprenamed toonly_apps— the field onGET /api/appsandGET /api/apps/manifestsnow 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 viaJob.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_statusfield (scheduled,waiting,completed, ormanual) instead of inferring state from a nullnext_run. One-shot jobs stay visible ascompletedafter firing instead of disappearing. (#1500) - EntityTime trigger — schedule a job driven by an entity's time-valued state (e.g.,
sun.sun'snext_rising). The job re-evaluates whenever the entity updates and stays inwaitingwhen the source entity is temporarily unavailable. (#1471)
API
Api.notify()andApi.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_registryandself.type_registrygive 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
startingstatus 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, andv0tags alongsidelatest(#1475)
Health & Observability
- Log persistence health reporting —
GET /api/healthnow reports whether log writes to the database are succeeding, alongside per-queue drop counts (log_queue_dropsanddb_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_keywarning 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.cachereplaced by instance-scopedApp.cache— cache access moves toApponly. 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 aself.cache.syncaccessor for sync equivalents.HassetteConfig.default_cache_sizeis replaced byHassetteConfig.default_cache_ttl(seconds).diskcacheis 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 fromApi,ApiSyncFacade,RecordingApi, andRecordingApiSyncFacade. Useapi.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 atapi.sync.helpers.*. (#1362)
Features
- replace
Resource.cachewith instance-scoped asyncApp.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-wto 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 —
nameis 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. OmittingnameraisesTypeErrorinstead of the oldListenerNameRequiredError— Pyright catches this at development time. All optional parameters must be passed by keyword. Update call sites to passname="..."explicitly and convert any positionalgroup=/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, matchingitems()and__iter__. Code that callslen(), indexes, or iterates multiple times must wrap the result inlist().iterkeys()anditervalues()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) ResourceRolestring values lowercased —"Core","Service","App","Base","Resource","Unknown"changed to"core","service","app","base","resource","unknown". Code comparingrole == "Core"must switch to lowercase. Enum member names are unchanged. (#1306)Api.get_state_value_typed()removed — also removed fromApiSyncFacade,RecordingApi, andRecordingApiSyncFacade. Useget_state().valuefor a typed value orget_state_value()for the raw string. (#1306)Api.toggle_servicerenamed totoggle— also renamed onApiSyncFacade,RecordingApi, andRecordingApiSyncFacade. Update calls fromapi.toggle_service(...)toapi.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 whenHassetteConfig.timezoneis unset. (#1327) - Pre-v1 backwards compatibility shims removed — deprecated aliases and transitional APIs are gone. (#1308)
Features
- add configurable timezone for wall-clock scheduling —
DailyandCrontriggers respectHassetteConfig.timezone, defaulting to the system timezone (#1327) - auto-derive service domain from entity ID and return typed results from entity methods — the manual
domainparameter is no longer needed oncall_service()(#1321)
Bug Fixes
- cli: fix
--sinceflag 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;
Appexposes ~20 names in autocomplete instead of ~54, hiding framework internals from app authors (#1314)
0.48.0 (2026-07-09)
⚠ BREAKING CHANGES
- remove
hassette eventCLI and events API endpoint (#1192)
Features
- add Run Now button for scheduled jobs (#1216) (54dd25d), closes #346
- add single-app manifest endpoint for app detail page (#1184) (020aed9), closes #368
- add time-since convenience properties to BaseState (#1212) (d8e96ba), closes #964
- add
where=predicate support to scheduler jobs (#1242) (8df4059), closes #1067 - export JSON Schema for
hassette.tomlconfig (#1186) (4f756a7), closes #663 - remove
hassette eventCLI and events API endpoint (#1192) (513389a), closes #915 #874 - route raising bus predicates to
on_errorhandlers and telemetry (#1259) (c4fe09a), closes #1255 - ui: add keyboard navigation to interactive tables (#1190) (97a70de), closes #750
- update typed state models for HA 2026.7 (#1197) (46359de), closes #1161
Bug Fixes
entity.turn_on()/turn_off()/toggle()now use thehomeassistantdomain (#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
/configmanifest data (#1171) (65966b7), closes #1139 - generated state model enum names now use canonical Python conventions (#1201) (7f507c7), closes #1199
contextvarsnow propagate to sync handler worker threads (#1172) (c414353), closes #1145
Performance Improvements
Documentation
0.47.0 (2026-07-02)
Breaking Changes
- Secret masking is now type-driven —
HassetteConfig.tokenis nowSecretStr | Noneinstead ofstr | None. Code reading the token must unwrap it withtoken.get_secret_value(). App-config secret masking follows the declared type: a field is masked only if it is typedSecretStr. The old name-based regex (token/password/secret/api_key) is removed. App authors who relied on name-based masking must type those fieldsSecretStrto 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 viajson_schema_extrauimetadata on theirAppConfigfields. (#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).enabledandautostartnow appear in the config tab. (#1154)
Bug Fixes
hassette app configshows 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_REGISTRYandTYPE_REGISTRYare now owned byhassette.conversioninstead ofhassette.models. Imports fromhassette.modelsstill work via re-exports but are deprecated; update tofrom hassette.conversion import STATE_REGISTRY, TYPE_REGISTRY. (#1122)
Apps
autostartconfig option — register an app without starting it at boot.enabled = true, autostart = falseregisters the app, shows it in the dashboard, and leaves it startable on demand, withenabledstill the hard on/off switch. These apps get a "no autostart" chip in the UI and a newAutostartcolumn inhassette app. (#1124)
Bus
- Per-listener backpressure policy — each subscription chooses
block(default; wait for a dispatch slot, unchanged behavior) ordrop_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_dispatchesconfig 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(), andis_home(entity_id), accessed viaself.states. Each reads the local state cache synchronously (noawait, no network round-trip) and checks thepersondomain, falling back todevice_trackerwhen nopersonentities exist. (#1130) - Domain-typed state access (
self.states.light["x"]/.get("x")) now raisesUnableToConvertStateErroron a conversion failure instead of leaking a rawpydantic.ValidationError. Code catchingValidationErrorfrom domain-typed access should catchUnableToConvertStateErrorinstead. (#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_eventsrespects hot-reload — it was frozen after the first dispatched event and silently ignored config changes; it now updates live likeconfig_log_level. (#1088)- Cold-start state retry now fires —
StateProxy.yield_domain_stateswas retrying a generator object rather than the readiness check, so the@retrynever re-ran before the state cache was ready. (#1088) - Wiring guards survive
python -O— service-wiringassertstatements (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 sameAppsubclass kept distinctdisplay_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
FAILEDduring teardown — a late submit-after-shutdown on an already-STOPPEDresource 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_leakedcount, 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
Appclass now resolve to distinctapp_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
singleexecution 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 passmode="parallel"on the registration (e.g.self.bus.on_state_change(..., mode="parallel")). Framework-tier listeners are unchanged and still default toparallel. (#1028)
Execution Overlap Modes
- Bus handlers gain per-listener overlap modes —
single(drop the re-fire),restart(cancel and replace),queued(serialize), andparallel(run concurrently) — controlling what happens when a handler is re-triggered while still running. App-tier defaults tosingle; framework-tier toparallel. (#1028) - Scheduled jobs gain the same overlap modes via a
mode=parameter onrun_in,run_once,run_every,run_daily, andrun_cron. Framework jobs default toparallel, app jobs tosingle. (#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_workersconfig. (#1042) - Blocking I/O run directly on the shared event loop (e.g.
time.sleep,requests.get, blockingopen().read()) is now detected, attributed to the offending app, and logged. Control it withblocking_io_behaviorglobally or per-app, or set it to"ignore"to disable. (#1040)
0.43.0 (2026-06-13)
Breaking Changes
- Duplicate
once=Truelisteners now raiseDuplicateListenerError— two once-listeners with the same name and topic no longer silently coexist. Use distinct names, or the newif_existsparameter, to register more than one under a shared name+topic. (#1015) Bus.add_listenernow returns aSubscriptioninstead ofNone— callers that asserted the return wasNonemust 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_existsparameter on bus handler registration —"error"(default) or"skip"for idempotent registration. (#1015)- Forgetting to
awaita 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
Comparisoncondition 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_waitretries 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/healthnow returns 200 (not 503) whenever the process is serving — forstartinganddegradedas well asok; 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 usingRestart=on-failurerestart 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
0.39.0 (2026-06-01)
⚠ BREAKING CHANGES
App.send_event(topic, data)andAppSync.send_event_sync(topic, data)are removed. Useself.bus.emit(topic, data)instead — same arguments, same behavior, now on the bus where subscriptions live.HassettePayloadno longer accepts anevent_typeparameter. Removeevent_type=from anyHassettePayload(...)orHassetteServiceEvent(...)construction.EventPayloadbase class also no longer hasevent_type— useHassPayload.event_typefor HA events, orevent.topicfor routing.
Features
- broadcast custom events between apps with
Bus.emit()and receive typed payloads viaD.EventData[T](#952) (082a59e)
Bug Fixes
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
Bug Fixes
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
0.35.0 (2026-05-27)
⚠ BREAKING CHANGES
hassettewith no arguments no longer starts the server. Usehassette runinstead. Thehassette servicecommand andGET /api/servicesendpoint have been removed.
Features
0.34.0 (2026-05-27)
Breaking Changes
state._has_feature(SomeFeature.FLAG)renamed tostate.has_feature()— remove the leading underscore. Thesupports_*convenience properties (e.g.,state.supports_brightness) are unchanged. (#891)Api.get_states_iterator()andApiSyncFacade.get_states_iterator()removed — the generator-based iterator does not work correctly in sync code. Useget_states()instead. (#891)
Logging
- Logging pipeline managed as a
LoggingServiceResource 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 queryoutput formatting — nested sub-models render as labeled sections instead of raw JSON,Noneshows 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_timeshowsin 3hfor future timestamps instead ofsoon. (#880)
Bug Fixes
- Fix 3 bugs in example apps where
BinarySensorState.valueandLightState.value(bothbool) 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. UseGET /api/executions/{execution_id}instead. The response shape (LogsByExecutionResponse) is unchanged. (#865)
CLI
hassette querysubcommand for querying all read-only API endpoints from the command line — apps, handlers, jobs, invocations, executions, logs, config, and sessions. (#838)--generate-completionreplaces--install-completion— outputs the shell completion script to stdout instead of modifying shell config files. Pipe to a file in your$fpathto install. (#870)- Fix zsh completion function name with cyclopts 4.16+ —
hassette --generate-completion zshnow produces the correct_hassettefunction 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_DIRenvironment variable toHASSETTE__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 fromHASSETTE__APP__*toHASSETTE__APPS__*. Python access changed fromconfig.app.*toconfig.apps.*. The API response field changed fromapptoapps. Update your hassette.toml, environment variables, and any code that accesses the config object.
Refactoring
0.31.0 (2026-05-20)
Breaking Changes
HassetteConfigrestructured into nested groups — all configuration fields moved under group prefixes. Environment variables change fromHASSETTE__LOG_LEVELtoHASSETTE__LOGGING__LOG_LEVEL,HASSETTE__DB_PATHtoHASSETTE__DATABASE__PATH, etc. TOML/YAML config files and programmaticconfig.*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 directHassetteConfigfield access to the nested format. Seesrc/hassette/config/legacy.pyfor the full old→new mapping. (#789)
Logging
- Replace
coloredlogswith a structlog-based logging pipeline — structured, context-rich log records with console and JSON formatters. All log I/O routed through an async queue soemit()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
HassetteConfigorganized into 8 nested groups:DatabaseConfig,WebSocketConfig,LoggingConfig,LifecycleConfig,WebApiConfig,AppConfig,SchedulerConfig,FileWatcherConfig. The/api/configREST 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 hassettewas 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
strare now typed with their domain StrEnum. StrEnums accept string construction sostate.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
Nonein HA source (deprecated) are no longer declared on the model. Values still land inmodel_extra(accessible via.extra("field_name")):LightAttributes.color_temp,max_mireds,min_mireds,xy_color;MediaPlayerAttributes.entity_picture_local. (#716) - Removed constraints —
LightAttributes.brightnessno longer hasgt=-1, lt=256field validators. (#716) hassette.models.states.featuresdeleted — IntFlag enums moved to per-domain files. All enums are still re-exported fromhassette.models.statessofrom hassette.models.states import LightEntityFeaturecontinues 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_validatorfor ZonedDateTime wire conversion, StrEnum attribute types, andsupports_*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.valuetype change for toggle entities —state.valuefor light, switch, fan, automation, humidifier, remote, script, siren, and update entities now returnsboolinstead ofstr. Code usingstate.value == "on"must change tostate.value is True. (#711)
State Models
- Audited all 50
BaseStatesubclasses 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
CancelledErrorno longer swallowed during task cancellation (#682) - REST API returns 404 (not 500) for non-existent
app_keyon 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_idchanged frominttostr(UUID4) — any user code comparingevent.payload.event_idagainst 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'scontext_idandorigincaptured alongside it. (#641) CURRENT_EXECUTION_IDContextVar 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
RestartSpecreplaces the global one-size-fits-all restart policy — each service declares its own restart type (PERMANENT,TRANSIENT, orTEMPORARY), sliding-window budget, backoff parameters, and error classification. The 5 globalservice_restart_*config fields are removed. (#638) EXHAUSTED_DEADandEXHAUSTED_COOLINGservice 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) ZonedDateTimevalues in REST API responses no longer include IANA timezone suffix ([America/Chicago]), fixing URL interpolation inget_historyandget_logbook(#619, #626)TaskBucketcrash logs now show meaningful task names (e.g.,AppLifecycleService.bootstrap_apps) instead ofTask-42(#623, #626)wait_forutility supports async predicates, eliminating hand-rolled async polling loops (#622, #626)AppTestHarnessper-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 implementTriggerProtocol. See the updated scheduler docs. (#517) RecordingApi.get_entityrequires explicit model argument — the previousBaseState-sentinel default silently aliased toget_state()and hid real bugs. Callers that want registry-converted state without a specific entity model should callget_state(entity_id)instead. (#525)- Docker startup uses constraints-based dependency protection — runtime
uv syncreplaced 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'sAppclass into testBus,Scheduler,StateManager, andRecordingApiwith zero boilerplate:async with AppTestHarness(MyApp, config={...}) as harness:(#492)RecordingApirecords write calls for assertions and delegates reads toStateProxy, withApiProtocolconformance 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_servicewith reliable task-bucket drain (#492, #525) - State seeding:
set_state,set_statesviaStateProxy._test_seed_state()(#492)
Scheduler
- Built-in trigger types:
After,Once,Every,Daily,Cron— all importable fromhassette.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=Truefires handlers at registration time when the target entity already matches predicates. Synthetic event hasold_state=None. Composes withonce,debounce,throttle. (#570)duration=Nholds require the entity to remain in the matching state for N continuous seconds before the handler fires. Consultslast_changedon restart for resilience. (#570)Bus.on_error(handler)andScheduler.on_error(handler)register app-level error handlers; per-registrationon_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_outshown as a distinct status in telemetry with amber warning badges (#552) timeoutandtimeout_disabledparameters on allSchedulerandBusregistration 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, andtimerentities (#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
TriggerProtocolsplit — custom triggers must now implement bothfirst_run_time(current_time)andnext_run_time(previous_run, current_time)instead of a single method. Triggers are now stateless. (#452)/api/healthzremoved — update Docker Compose health checks from/api/healthzto/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. Passif_exists="skip"for idempotent registration. (#297) once=True+ rate limiting raisesValueError— combiningonce=Truewithdebounceorthrottleis 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/healthreturns 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=Truelisteners cannot double-fire under rapid events (#430) - Zero or negative
debounceandthrottlevalues rejected withValueErrorat registration (#430) if_existsparameter on allScheduler.run_*methods —"error"(default) or"skip"for idempotent registration (#297)IntervalTriggerrejects zero/negative intervals withValueErrorat construction (#452)
Logging
- Per-service log level tuning via 13 dedicated
*_log_levelconfig 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)
CommandExecutorstartup crash resolved — registration methods wait forDatabaseServicereadiness before DB access (#330)- SQLite write serialization eliminates
OperationalError: cannot commit transactionraces 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_modeflag (#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_keymismatch fix (#335, #336)
Documentation
- Fixed broken code examples:
run_hourly(minute=15)TypeError, invalidapi_portfield, wronginstance_namekey in config snippet (#469, #472) - Added parameter tables to all scheduler methods with types, defaults, and cross-references (#469, #472)
- Corrected
data_dirdefault from~/.hassetteto 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.mdpage (#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.csswith[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-expandedfor accessibility (#262) - E2E tests now run by default with
uv run pytestinstead of requiring-m e2e; addednox -s e2esession for CI HassetteHarnessnow 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_utilspublic API; tests import from the package instead of submodules (#253) - Replaced 28
asyncio.sleep()synchronization calls across 8 integration test files withwait_forpolling helper for deterministic, faster tests (#253) - Renamed
create_mock_hassette()tocreate_hassette_stub()andmock_hassette.pytoweb_mocks.pyto 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-codebackground (#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()andmodel_dump_json()onAppManifestandHassetteConfigno 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
LockStatemodule withLockAttributesandLockEntityFeature(#294) supports_*boolean properties on light, climate, cover, fan, media_player, and vacuum attribute classes for checking entity capabilities without manual bitmask operations (#272)IntFlagenums (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--ghostandht-btn--xsbutton modifier classes (#262)extrasproperty andextra()helper onBaseStateandAttributesBasefor 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:jstasks (#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-issuesClaude 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_reloadconfig 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_uiconfig option to enable/disable the UI independently from the API- Added section to docs covering the web UI
- FastAPI web backend replacing the standalone
HealthServicewith a full REST API and WebSocket server GET /api/health,GET /api/healthz— system health and container healthchecksGET /api/entities,GET /api/entities/{entity_id},GET /api/entities/domain/{domain}— entity state accessGET /api/apps,GET /api/apps/{app_key},GET /api/apps/manifests— app status and manifestsPOST /api/apps/{app_key}/start|stop|reload— app managementGET /api/scheduler/jobs,GET /api/scheduler/history— scheduled jobs and execution historyGET /api/bus/listeners,GET /api/bus/metrics— per-listener execution metrics and aggregate summaryGET /api/events/recent,GET /api/logs/recent,GET /api/services,GET /api/config— events, logs, HA services, configGET /api/ws— WebSocket endpoint for real-time state/event/log streaming with subscription controlsGET /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_multiplierconfig optionsscheduler_behind_schedule_threshold_secondsconfig 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
HealthServicewithWebApiServicebacked 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 Servicebase class now properly sequencesserve()task lifecycle: spawns afteron_initialize(), cancels beforeon_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
AppHandlerinto four focused components:AppRegistry(state tracking),AppFactory(instance creation),AppLifecycleManager(init/shutdown orchestration), andAppChangeDetector(configuration diffing) - File watcher now batches multiple file change events to prevent race conditions (
changed_file_pathpayload is nowchanged_file_paths: frozenset[Path]) - Renamed
active_apps_configtoactive_manifestsonAppRegistry AppManifest.app_confignow accepts both"config"and"app_config"keys
Added
HassetteAppStateEventemitted when app instances change status (includes app_key, status, previous_status, exception details)- New
Busconvenience methods:on_app_state_changed(),on_app_running(),on_app_stopping() BlockReasonenum and blocked app tracking inAppRegistryto distinguish "enabled but excluded by@only_app" from "not configured"ResourceStatus.STOPPINGenum valueenabled_manifestsproperty onAppRegistryfor querying enabled apps regardless ofonly_appfilterStateManager.get(entity_id)for generic entity access with automatic domain-type resolution andBaseStatefallback
Fixed
- Removing
@only_appdecorator 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
sourcenow optional inAutomationTriggeredPayload
[0.20.2] - 2026-02-01
Changed
- rename parameter
comparatortoopinComparisoncondition
[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, anditemsmethods to StateManager and StateRegistry - Add functionality to route
state_changeevents to more specific handlers based on domain and/or entity_id - This is done automatically by the
Busby 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
AnnotationConverterclass andTypeMatcherclass for more robust validation/conversion during DI - Add A, P, C, and D aliases to
hassette.__init__for simpler imports A=hassette.event_handling.accessorsP=hassette.event_handling.predicatesC=hassette.event_handling.conditionsD=hassette.event_handling.dependencies- Add new
Comparisoncondition 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 attrsget_all_attrs_<old|new|old_new>- get all attributes as a dict- Add
get_all_changesaccessor 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
BaseStatewhen we do not find a class in theStateRegistry - This does not work because
BaseStatedoesn't have adomain - 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
MediaPlayerStatenow hasattributesusing the correct type
Changed
- BREAKING: Replaced
StateManager.get_stateswith__getitem__that accepts a state class - The error raised in StateManager when a state class is not found in the
StateRegistrynow advises to use this method - Renamed
LOG_LEVELStoLOG_LEVEL_TYPE - Renamed
get_default_dicttoget_defaults_dictto be more clear this is not referring todefaultdictclass - Use same validation for
AppConfiglog level as we do forHassetteconfig log level - Extracted nested Attributes classes for each state out of class definition to make them first class citizens
- e.g.
MediaPlayerState.Attributesis nowMediaPlayerAttributes
Docs
- Remove
Why Hassettepage - 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.pyito fix type hints
[0.19.0] - 2026-01-25
Fixed
- Exit
TypeRegistry.convertearly if already a valid type - Avoid mutating state dicts when accessing via
DomainStates
Added
- Add
__contains__method to DomainStates - Allows us to use
inchecks - Add
to_dict,keys,values, anditemsmethods to DomainStates - Provides convenient access to entity IDs and typed states
- Add
yield_domain_statesto StateProxy - Allows iterating over all states in the proxy
- Handles KeyError when extracting domain
- Update
DomainStatesclass to accept aStateProxyinstance instead of state dictionary to ensure it stays up to date - Add caching to
StateManager, holding on to eachDomainStatesinstance after creation - Add caching to
DomainStates, usingfrozendict.deepfreezeto hash the state dict and avoid recreating the instance if it has not changed
Removed
- BREAKING: Remove
_TypedStateGetterclass and correspondinggetmethod onStateManager- this was never a good idea due to its confusing api - BREAKING: Remove
allproperty onStateManager- 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
latesttag, 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
StateChangeEventis nowRawStateChangeEvent- There is a new DI handler for
TypedStateChangeEventto handle conversion if desired - Breaking: State conversion system now uses dynamic registry instead of hardcoded unions
StateUniontype has been removed - useBaseStatein type hints insteadDomainLiteraltype has been removed - no longer needed with dynamic registration- State classes automatically register their domains via
__init_subclass__hook - Breaking:
try_convert_statenow typed to returnBaseState | Noneinstead ofStateUnion | None - Uses registry lookup instead of Pydantic discriminated unions for conversion
- Falls back to
BaseStatefor unknown/custom domains try_convert_statemoved tohassette.state_registrymodulestates.__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.pytostate_manager.py(and renamed the class) to avoid confusion withmodels/statesmodule - 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, andpredicatesall tohassette.event_handlingfor consistency - Moved DI extraction and injection modules to
hassette.bus
Added
TypeRegistryclass 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
AnnotatedDI handlers
Removed
- Breaking: Removed
StateUniontype - replaced withBaseStatethroughout codebase - Breaking: Removed
DomainLiteraltype - no longer needed with registry system - Breaking: Removed manual
_StateUniontype 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 providedaccessorsmodule - They were too difficult to maintain/type properly across the framework
- These can be used still by annotating with
[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.pytocore.py - Renamed
servicestocoreand movecore.pyundercoredirectory - Didn't make sense to keep named as
servicessince we have resources in here as well
Added
- Add
diskcachedependency andcacheattribute to all resources - Each resource class has its own cache directory under the Hassette data directory
- Add
statesattribute toApp- provides access to current states in Home Assistant statesis an instance of the newStatesclassStatesprovides domain-based access to entity states, e.g.app.states.light.get("light.my_light")Stateslistens 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
NoneorMISSING_VALUEto 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_typetoHassetteConfigto control whether to raise an error if a dependency cannot be provided due to type mismatch - Default is
truein production mode,falsein dev mode - When
falsea 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
DomainLiteralinhassette.models.states.base - Add tests to catch this in the future
[0.16.0] - 2025-11-16
Added
- Added
ANY_VALUEsentinel for clearer semantics in predicates - use this to indicate "any value is acceptable" - Dependency Injection for Event Handlers - Handlers can now use
Annotatedtype hints with dependency markers fromhassette.dependenciesto 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.dependenciesmodule documentation and updated examples for details
Changed
- Breaking: - Event handlers can no longer receive positional only args or variadic positional args
NOT_PROVIDEDpredicate is now used only to indicate that a parameter was not provided to a function
[0.15.5] - 2025-11-14
Changed
- Update
HassetteConfigdefaults 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 = truein dev mode) - Moved
AppManifestandHassetteTomlConfigSettingsSourcetoclasses.py - Moved
LOG_LEVELStohassette.types.typesinstead ofconst.misc, as this is aLiteral, not a list of constants - Renamed
core_config.pytocore.py - Bumped version of
uvinmise.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_changeswas 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
entitiesback to public API exports fromhassette
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.errorand short traceback instead oflogger.exception
[0.15.3] - 2025-11-02
Changed
- Moved more internal log lines to
DEBUGlevel to reduce noise during normal operation. - Moved
only_appwarning to only emit if@only_appis actually being used. - Make
FalseySentinelsubclass to use forNOT_PROVIDEDandMISSING_VALUEto simplify bool checks. - Add
Typeguardmethod toStateChangePayloadto allow type narrowing onold_stateandnew_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.IncreasedandDecreasedconditions added for numeric comparisons.- Added
IsNoneandIsNotNoneconditions for checking if a value isNoneor 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 = falsein 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
--helpworks 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.tomlto 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 thechangedparameter onon_state_changeandon_attribute_changemethods. - 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
predicatesmodule - Recommendation now is to import like
from hassette import predicates as Pfrom hassette import conditions as Cfrom hassette import accessors as A
- Breaking: -
base_urlnow requires an explicit schema (http:// or https://) - If no schema is provided, a
SchemeRequiredInBaseUrlErrorwill be raised during config validation - This is to avoid having to guess the intended scheme, which can lead to confusion and errors
- Breaking: -
base_urlmust haveportincluded 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._websockettoHassette._websocket_serviceto match naming conventions. - Refactor handler types and move types into
typesmodule 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
.pyif 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.pyrenamed totask_bucket.pyto follow naming conventionspost_to_loopmethod added toTaskBucketto allow posting callables to the event loop from other threads
Changed
- Breaking: - Renamed
async_utils.pytofunc_utils.py, addedcallable_nameandcallable_short_nameutility functions - Breaking: - Upgrade to
whenever==0.9.*which removedSystemDateTimein favor ofZonedDateTime- 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, andrun_dailyto not start immediately if nostartwas provided, but to start on the next interval instead.
Bus/Predicate Refactor
- Refactored predicates to use composable
Predicates,Conditions, andAccessors Predicateis a callable that takes an event and returns a bool- E.g.
AttrFrom,AttrTo,DomainMatches,EntityMatches,ValueIs, etc.
- E.g.
Conditionis a callable that takes a value and returns a bool- E.g.
Glob,Contains,IsIn,IsOrContains,Intersects, etc.
- E.g.
Accessoris a callable that takes an event and returns a value- E.g.
get_domain,get_entity_id,get_service_data,get_path, etc.
- E.g.
- 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
Subscriptionnow hascancelmethod to unsubscribe from events, to be consistent withScheduledJob.App.send_event_syncmethod added for synchronous event sending.Bus.on_status_change,Bus.on_attribute_change,Bus.on_service_callall take sync callables for comparison parameters.- For example, you can pass a lambda to
changed_fromthat does a custom comparison. Busnow exposeson_homeassistant_stopandon_homeassistant_startconvenience methods for listening to these common events.Busstatus/attribute change entity_id parameters now accept glob patterns.
Changed
- Breaking:
Scheduler.run_oncehas been updated to usestartinstead ofrun_atto be consistent with other helpers. - Breaking:
cleanupmethod is now marked as final and cannot be overridden in subclasses. - Breaking:
Bus.on_entityrenamed toBus.on_status_changeto match naming conventions across the codebase. - Breaking:
Bus.on_status_changeentityparameter renamed toentity_idfor clarity. - Breaking:
Bus.on_attributerenamed toBus.on_attribute_changeto match naming conventions across the codebase. - Breaking:
Bus.on_attribute_changeentityparameter renamed toentity_idfor clarity.
Removed
- Breaking: Removed deprecated
set_logger_to_debugandset_logger_to_levelResource methods. - Breaking: Removed deprecated
run_sync,run_on_loop_thread, andcreate_taskmethods from Hassette. - Breaking: Removed
run_atalias forrun_oncein 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, andChangeType.PredicateCallableis a callable that takes a single argument of any known type and returns a bool.KnownTypesis a union of all types that can be passed to predicates.ChangeTypeis a union of all types that can be passed to change parameters.
- Use
Sentinelfromtyping_extensionsfor default values. - Rename
SENTINELtoNOT_PROVIDEDfor clarity. - Moved
is_async_callabletohassette.utils.async_utils, now being used in more places. - Moved glob logic from
Routertohassette.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_dailyscheduler 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_initializeandon/before/after_shutdownadded toResourceandServicefor more granular control over startup and shutdown sequences. - Breaking:
App.initializeandApp.shutdownare now final methods that call the new hooks; attempting to override them will raise aCannotOverrideFinalError. - 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, andrun_dailyfor common cadence patterns. - Add
humanizeto support human-friendly duration strings in log messages.
- Dev Mode:
- Reintroduced
dev_modeconfiguration flag (also auto-enabled when running under a debugger orpython -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 withalways_reload_appsconfig flag. - Only respect
@only_appdecorator when indev_mode, to avoid accidentally running only one app in production - overridable withallow_only_app_in_prodconfig flag. - The event loop automatically switches to debug mode when
dev_modeis enabled. - Task Buckets:
- Task buckets gained context helpers and
run_sync/run_on_loop_threadwrappers 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 inhassette.utils.async_utilsso sync callables are wrapped with the owning bucket's executor. - App-owned
Api,Bus, andSchedulerinstances 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_bucketto spawn background tasks and to run synchronous code, to ensure proper tracking and cancellation.- Using
self.hassette.run_syncorself.hassette.run_on_loop_threadis still supported, but will not track tasks in the app's task bucket.
- Using
- Configuration:
- Resolve all paths in
HassetteConfigto absolute paths. - Individual service log levels can be set via config, with the overall
log_levelbeing used if not specified. - New config options for individual service log levels:
bus_service_log_levelscheduler_service_log_levelapp_handler_log_levelhealth_service_log_levelwebsocket_log_levelservice_watcher_log_levelfile_watcher_log_leveltask_bucket_log_levelapps_log_level- Add
log_leveltoAppConfigso 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 tolog_all_eventsif not set.log_all_hassette_events- log every event from Hassette apps and core - will fall back tolog_all_eventsif not set.
- Add
app_startup_timeout_secondsandapp_shutdown_timeout_secondstoHassetteConfigto 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_domainsandbus_excluded_domainsconfig 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
hassettepackage; the oldhassette.corepaths have been moved underhassette.core.resources/hassette.core.services, so update any directhassette.core...imports to use the re-exported names onhassette. - Breaking:
App.initializeandApp.shutdownhave been replaced withApp.on_initializeandApp.on_shutdownhooks that do not need to callsuper(). - 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_childand harmonizedcreate()factory methods, so services and sub-resources inherit owners and task buckets automatically. Api.call_serviceand the sync facade default toreturn_response=False, and theturn_on/turn_off/toggleare corrected to not passreturn_responsesince this is not supported.- Deprecated
set_logger_to_level- loggers are finally working properly now so the standardlogger.setLevel(...)should be used instead.
Fixed
- Event bus and scheduler loops respect
shutdown_event, allowing them to exit promptly during shutdown. - WebSocket reconnects treat
CouldNotFindHomeAssistantErroras retryable and properly apply the retry policy, improving cold-start resilience. Api.call_servicenow includesreturn_responsein the payload when requested, andServiceResponsecorrectly 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
resourcesandservices - Use
contextvarsinstead of class variables to track global instance ofHassetteandHassetteConfig SchedulerServicenow delegates scheduling to_ScheduledJobQueue, which uses a fair async lock to coordinate concurrent writers before dispatching due jobs.Hassette.run_sync/run_on_loop_threadnow route through the global task bucket.- Breaking: The
run_forevermethod of theServiceclass has been replaced withserve. The new lifecycle hooks are valid forServiceas well.
[0.11.0] - 2025-10-05
Added
hassette.event.app_reload_completednow fires after reload cycles, andHassetteEmptyPayloadprovides a helper for simple internal events.- Add
TaskBucketclass for tracking and cancelling related async tasks. - Add
Hassette.task_bucketfor global task tracking, andResource.task_bucketfor per-resource task tracking. - Introduced
TaskBucketinstances for Hassette, services, and apps; configure shutdown grace periods via the newHassetteConfig.task_cancellation_timeout_secondssetting. - Added
Hassette.wait_for_readyandhassette.utils.wait_for_readyhelpers so resources can block on dependencies (for example, the API now waits for the WebSocket). - Add
ResourceNotReadyErrorexception 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, andscheduler_min/default/max_delay_seconds. - Add individual log level settings for core services.
- Add
cleanuplifecycle method toResourceandServicefor 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 throughBusService, which now tracks listeners by owner under a fair async lock for atomic cleanup. - Breaking:
@onlybecomes@only_app, apps must expose a non-emptyinstance_name, and each app now owns itsBusandSchedulerhandles. - Breaking: The
hassette.core.appspackage moved underhassette.core.classes.app, and the service singletons are nowBusServiceandSchedulerService; import apps fromhassette.core/hassette.core.classesand treat the underscored services as private. - Deprecated:
set_logger_to_debughas been renamed toset_logger_to_level, and all core services now default toINFOlevel logging.set_logger_to_debugis still available but will be removed in a future release. - App handlers now mark apps as ready after
initializecompletes. - 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_nothingfixture, and continues to provision mock services so CI can run without a Home Assistant container. - Tightened local tooling: expanded
pyrightconfig.json, enabled Ruff'sTID252, and taught the nox test session to runpytestwith-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/Servicewith 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
SystemDateTimeconsistently instead ofInstantor mixed types - Replaced deprecated
InstantBaseStatewithDateTimeBaseStatefor better type handling - Remove
repr=Falseforlast_changed,last_updated, andlast_reportedinBaseStateto improve logging and debugging output
Fixed
- Fixed incorrect datetime conversion in
InputDateTimeStateto ensure proper timezone handling
[0.9.0] - 2025-09-26
Added
- Added ability to provide args and kwargs to scheduled jobs via scheduler helpers
argsandkwargskeyword-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_imageandget_calendar_eventsto excludedatetime,date, andZonedDateTime- usePlainDateTime,SystemDateTime, orDateinstead
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
watchfileslibrary - 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 = falsein 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_runningfunction added toHassetteclass- also available as standalone utility function in
hassette.utils @onlydecorator to allow marking a single app to run without changinghassette.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_keytoAppManifest- 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_servicetarget typing to also allow lists of ids - thanks @zlangbert!
[0.7.0] - 2025-09-14
Changed
- rename
cancelonSubscriptiontounsubscribefor clarity
Fixed
- improved docstrings across
Apimethods
Added
- Documentation!
[0.6.2] - 2025-09-14
Fixed
- Fix logging on
Appsubclasses to usehassette.<AppClassName>logger
[0.6.1] - 2025-09-14
Fixed
- Fixed
HassetteConfigusing 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
HassetteConfigincorrectly prioritizingHASSETTE_LOG_LEVELoverHASSETTE__LOG_LEVEL(double underscore should take precedence)
[0.6.0] - 2025-09-14
Removed
- Removed
DEFAULT_CONFIGconstant for app config, not necessary
Fixed
- Fixed
HassetteConfigto properly handleenv_fileandconfig_fileparameters passed in programmatically or via CLI args - These are now passed to the appropriate settings sources correctly
- Fixed
HassetteConfigincorrectly 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 customTomlConfigSettingsSource - 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.ymlfile - Update README with example of setting app config inline (.e.g
config = {send_alert = true}) - Added relative
./configpath 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
isinstancedoes not work consistently, will be providing recommendation in docs on how to make this work better
- Known Issue: Using
Hassette
- Update imports to be relative, same as other modules
Apps
- Rename
app_manifest_clstoapp_manifest- was always an instance, not a class
Added
HassetteConfig
- Add
secretsattribute toHassetteConfigto 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"]
- Secrets can be listed in the config file like
- Add
HassetteBaseSettingsto add tracking of final settings sources for all config attributesHassetteConfig.FINAL_SETTINGS_SOURCESwill show where each config attribute was set from- Useful for debugging config issues
- Add
HassetteTomlConfigSettingsSourceto load config from a TOML file, supports top level[hassette]section - Add
get_configclass method toHassetteConfigto get global configuration without needing to accessHassettedirectly- E.g.
HassetteConfig.get_config()will return the current config instance
- E.g.
- 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_apponHassetteclass to allow getting an app instance by name and index (if necessary)- E.g.
hassette.get_app("MyApp")orhassette.get_app("MyApp", 1)
- E.g.
[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
TomlConfigSettingsSourcethat handles this - BREAKING: Switch back to
__double underscore for environment variable prefixes, prevents issues with app config that uses single underscore - Add
env_fileto AppConfig default class config to load environment variables from/config/.envand.envfiles automatically - Add examples of using
SettingsConfigDictto set a customenv_prefixon 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_dirconfiguration 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 HealthServiceconfig - allow setting port and allow disabling health servicehealth_service_port(default: 8126)run_health_service(default: true)
Changed
- BREAKING: Moved all event models from
hassette.models.eventstohassette.core.eventsfor better organization - BREAKING: Updated configuration structure - flattened Hass configuration properties directly into main config
config.hass.token→config.tokenconfig.hass.ws_url→config.ws_urlconfig.hass.base_url→config.base_url- BREAKING: Changed environment variable prefix from
hassette__*tohassette_*(double underscore to single) - Change resource status prior to sending event, to ensure consistency
- Improve retry logic in
_Apiand_Websocketclasses
Fixed
- Improved App constructor with better parameter formatting and documentation
- Added
indexparameter 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
discriminatorfield - Improved error handling in
try_convert_statefunction - Updated AppConfig to allow arbitrary types (
arbitrary_types_allowed=True) - Handle bug in
HealthServiceconfig - sometimesweb.AppKeyraises anUnboundLocalError(only seen in testing so far), fallback to string in this case
Removed
- Removed unused
_make_unique_namemethod from App class - Removed
KNOWN_TOPICSconstant that was no longer used - Removed
hass_configproperty from Hassette class (configuration is now flattened) - Cleaned up unused imports and redundant code
ResourceSyncstopmethod on Resource__init__onServicethat 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_pathproperty 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_classutility function so we don't attempt to usetyping.TypeVaras a config class.
[0.3.2] - 2025-09-07
Fixed
- Removed incorrect
__init__override inAppSyncthat was causing issues with app instantiation
[0.3.1] - 2025-09-07
Fixed
- Fixed timestamp conversion return types in
InputDateTimeStateattributes - Removed custom attributes from input number states
- Get AppSync working using anyio.to_thread and
hassette.loop.create_taskto ensure we're on the right event loop
Internal
- Consolidated input entity states into unified
input.pymodule BinarySensorStatenow inherits fromBoolBaseState- Fixed inheritance issues in
SceneState,ZoneState, andNumberState - Update health service to use a
web.AppKeyinstead 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,predicatesexports - E.g.
from hassette import AttrChangedbecomesfrom hassette import predicatesandpredicates.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.tomlconfiguration file - New
on_hassette_service_startedevent 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
HassetteServiceEventannotation
[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)