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.
- Scheduler job and listener filters in web UI partials and API now return correct results by fixing
owner_id/app_keymismatch in DB recording and filter comparisons (#335) (#336) get_job_summarytelemetry query now returnsjob_id,app_key, andinstance_indexcolumns, matching the shape ofget_listener_summary(#334)- Bus page telemetry query failures (from
asyncio.gather) are now logged at WARNING instead of being silently dropped (#334) - App detail and instance detail pages now correctly filter scheduled jobs by
owner_idinstead ofapp_key, so jobs appear in the app detail view (#334) - Scheduler and bus page app-filter dropdowns now correctly pass
app_keyto HTMX partials (param name wasowner, never matched the endpoint) (#334) ListenerMetricsResponsenow usesapp_key/instance_indexfields instead ofowner, matching the data produced byTelemetryQueryService(#334)DatabaseServicenow serializes all SQLite writes through a single-writer queue, eliminatingOperationalError: cannot commit transaction - SQL statements in progressrace conditions at startup (#331, #333)CommandExecutorstartup crash:register_listener()/register_job()now wait forDatabaseServiceto be ready before accessing.db, andexecute()no longer raisesRuntimeErrorwhen handlers fire before_create_session()completes (#330)
TelemetryQueryServiceserves real SQLite-backed telemetry (listener invocation counts, job execution counts, last error, last run) from the database instead of stubs (#267) (#334)RuntimeQueryServicereplacesDataSyncServiceas the in-memory web UI data layer, aggregating app status, event buffer, log buffer, and WebSocket broadcast (#267) (#334)- Docker-based startup smoke tests that run Hassette against a real Home Assistant container, verifying WebSocket connect, session creation, entity visibility, event bus firing, and sentinel record integrity (#332)
CommandExecutorservice consolidates handler invocation and scheduled job execution with unified timing, error classification, and batched SQLite telemetry writes (#329)DatabaseServicewith Alembic migration infrastructure for persistent SQLite telemetry storage (sessions, listeners, scheduled jobs, handler invocations, job executions) (#305)- Event injection and lifecycle reset utilities in
test_utilsfor service-level integration tests (emit_service_event,emit_file_change_event,make_service_failed_event,make_service_running_event,wire_up_app_state_listener,wire_up_app_running_listener,reset_hassette_lifecycle) (#303) App.app_keyproperty for accessing the app's configuration key (#297)if_existsparameter on allScheduler.run_*methods with"error"(default) and"skip"modes for idempotent job registration (#297)ScheduledJob.matches()method for comparing job configuration (callable, trigger, repeat, args, kwargs) (#297)__eq__/__hash__onIntervalTriggerandCronTriggercomparing configuration only (interval/cron expression) (#297)
- Breaking: Scheduler now enforces job name uniqueness per instance, raising
ValueErroron duplicate names (#297) - Breaking: Renamed
ExecutionResult.started_attomonotonic_startto clarify it stores a monotonic clock value (#297)
- Await
Bus.remove_all_listeners()inServiceWatcher,StateProxy, andAppHandlershutdown to prevent listener cleanup race condition (#311) - StateProxy
on_disconnectnow usesremove_job()instead ofcancel()to properly free the job name slot for reconnection (#297) - Strip literal quote characters from
base_urlbefore parsing, fixing connection failures when Docker Compose passes quoted env var values (#298) - ServiceWatcher now triggers Hassette shutdown when a service exceeds its max restart attempts, instead of silently giving up (#301)
- 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)
- 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)
- 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)
- Bulma CSS CDN dependency (#262)
- Entity Browser page and related partials (#262)
- 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
- Live dashboard/page updates now use WebSocket push with idiomorph DOM morphing instead of 30-second polling intervals
- 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 serverGET /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
ServiceWatcherservice_restart_max_attempts,service_restart_backoff_seconds,service_restart_max_backoff_seconds,service_restart_backoff_multiplierconfig options
scheduler_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)
- 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()
- 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
HealthService(src/hassette/core/health_service.py) — replaced by FastAPI web backend
- 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
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
- Removing
@only_appdecorator now correctly starts previously-blocked apps during hot reload
- Fixed finding of requirements files in Docker image, thanks @mlsteele!
- Added tests to ensure requirements files are found correctly in Docker image
sourcenow optional inAutomationTriggeredPayload
- rename parameter
comparatortoopinComparisoncondition
- add back activation of virtualenv in docker startup script
- 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
- This is done automatically by the
- Add
AnnotationConverterclass andTypeMatcherclass for more robust validation/conversion during DI - Add A, P, C, and D aliases to
hassette.__init__for simpler importsA=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
- 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
- This does not work because
- 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
- 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
- The error raised in StateManager when a state class is not found in the
- 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
- e.g.
- Remove
Why Hassettepage - Remove docker networking page
- Very large cleanup/reorg/addition of docs
- Change log level for state cache loading message from INFO to DEBUG
- Update
state_manager.pyito fix type hints
- Exit
TypeRegistry.convertearly if already a valid type - Avoid mutating state dicts when accessing via
DomainStates
- Add
__contains__method to DomainStates- Allows us to use
inchecks
- Allows us to use
- 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
- 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
- Improve docker startup script and dependency handling
- Rewrite docker docs to be more clear about project structure and dependency installation
- 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
- Hardcode UID/GID of 1000 for non-root user in Docker image
- Breaking: Docker image switched to Debian slim
- Breaking: Remove
latesttag, latest tag will now include python version as well
- Use correct version of python when pulling base image
- (e.g. image tagged with py-3.12 uses python 3.12)
- 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
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
- 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
- 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.
- This allows use of
- 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
- Didn't make sense to keep named as
- 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 Assistantstatesis 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 handlersMaybeStateNew,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
- Default is
- 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
- 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
- Available dependencies include
- 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
- 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)
- Generally speaking, values are extended (e.g. timeouts) and more permissive (e.g.
- 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 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
- Now uses
- 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
- 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
- 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>)
- Implemented as
- Improved documentation landing page
- Add logo
- Improve getting-started page
- Fix docker_start.sh to use new entrypoint
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.
- This can be disabled by setting
- 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.
- 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
- 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
- Recommendation now is to import like
- 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
- If no schema is provided, a
- 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
- 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.
- 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
- 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
- New type for handlers,
HandlerType, as we now have additional protocols for variadic handlers
- Correct scheduler helpers
run_minutely,run_hourly, andrun_dailyto not start immediately if nostartwas provided, but to start on the next interval instead.
- Refactored predicates to use composable
Predicates,Conditions, andAccessorsPredicateis 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
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.
- For example, you can pass a lambda to
Busnow exposeson_homeassistant_stopandon_homeassistant_startconvenience methods for listening to these common events.Busstatus/attribute change entity_id parameters now accept glob patterns.
- 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.
- 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.
- 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.
- Updated Apps and Scheduler documentation to reflect new features and changes.
- Improved reference docs created with autodoc.
- Fixed
run_minutely/run_hourly/run_dailyscheduler helpers to run every N minutes/hours/days, not every minute/hour/day at 0th second/minute.
- 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.
- Lifecycle hooks
- 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.
- Hassette now performs a pre-check of all apps before starting, exiting early if any apps raise an exception during import.
- 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.
- Reintroduced
- 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
- Task buckets gained context helpers and
- 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.
- Resolve all paths in
- 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.
- Attempting to override these methods will now raise a
- 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.
- 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.
- 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.
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.
- 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.
- 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.
- 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.
- Added utility functions for datetime conversion in
src/hassette/utils.py
- 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 incorrect datetime conversion in
InputDateTimeStateto ensure proper timezone handling
- 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
- Narrow date/time types accepted by
get_history,get_logbook,get_camera_imageandget_calendar_eventsto excludedatetime,date, andZonedDateTime- usePlainDateTime,SystemDateTime, orDateinstead
- 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
- Remove opengraph sphinx extension from docs dependencies - it was causing issues with building the docs and isn't necessary for our use case
- 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
- importable from
- add
app_keytoAppManifest- reflects the key used to identify the app in config
- move service watching logic to it's own service
- refactor app_handler to handle reloading apps, re-importing, removing orphans, etc.
- update
api.call_servicetarget typing to also allow lists of ids - thanks @zlangbert!
- rename
cancelonSubscriptiontounsubscribefor clarity
- improved docstrings across
Apimethods
- Documentation!
- Fix logging on
Appsubclasses to usehassette.<AppClassName>logger
- 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)
- Removed
DEFAULT_CONFIGconstant for app config, not necessary
- 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)
- 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
- 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
- Update imports to be relative, same as other modules
- Rename
app_manifest_clstoapp_manifest- was always an instance, not a class
- 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
- 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.
- 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
- Update examples to remove references to top level
- 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
- Fixed permissions for /app and /data in Dockerfile
- Update example docker-compose.yml to use named volume for /data
- Fixed Dockerfile to build for both amd64 and arm64
- 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
- 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)
- 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
- 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 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
- 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
- Filter pydantic args correctly in
get_app_config_classutility function so we don't attempt to usetyping.TypeVaras a config class.
- Removed incorrect
__init__override inAppSyncthat was causing issues with app instantiation
- 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
- 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
- get tests against HA instance working in Github Actions
- updated tests for fixed synchronous app handling
- 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
- E.g.
- 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
- 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 notify service call examples in battery and presence apps
- Fixed
HassetteServiceEventannotation
- 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
- 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
- API parity between sync and async methods
- Sensor attribute handling and device class support
- Configuration scope and core initialization
- 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
- 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
- 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)