Skip to content

refactor(gateway): migrate Home Assistant adapter to bundled plugin#32500

Closed
kshitijk4poor wants to merge 1 commit into
NousResearch:mainfrom
kshitijk4poor:feat/homeassistant-platform-plugin
Closed

refactor(gateway): migrate Home Assistant adapter to bundled plugin#32500
kshitijk4poor wants to merge 1 commit into
NousResearch:mainfrom
kshitijk4poor:feat/homeassistant-platform-plugin

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

Moves the Home Assistant gateway adapter out of gateway/platforms/homeassistant.py and into plugins/platforms/homeassistant/ as a bundled plugin, following the same shape as the recent Mattermost (#31748), Discord, Teams, and other platform-plugin migrations.

After this PR, every consumer of the HA adapter goes through PluginManager.discover_and_load()platform_registry.get("homeassistant") instead of a hardcoded Platform.HOMEASSISTANT import branch.

What moves into the plugin

In core today Moves to Notes
gateway/platforms/homeassistant.py (the whole file) plugins/platforms/homeassistant/adapter.py git mv, 76% similarity preserved
gateway/run.py HOMEASSISTANT elif in build_adapter() registry path removes the late-import branch
tools/send_message_tool.py::_send_homeassistant _standalone_send() in plugin reachable from cron via standalone_sender_fn
HOMEASSISTANT elif in _send_to_platform chain registry else branch same path mattermost uses today

New files:

  • plugins/platforms/homeassistant/__init__.py — exposes register
  • plugins/platforms/homeassistant/plugin.yaml — manifest declaring HASS_TOKEN (required) and HASS_URL (optional)
  • plugins/platforms/homeassistant/adapter.py — renamed adapter with an appended register() block

What stays in core (intentionally)

  • The HASS_TOKEN / HASS_URL env-to-PlatformConfig seeding at gateway/config.py:1392-1401. The bluebubbles, mattermost, and discord migrations all kept their equivalent block in gateway/config.py rather than moving it into an env_enablement_fn. Keeping the same pattern here means PR feat(homeassistant): split tool vs platform env vars #29199 (split tool vs platform env vars) collides at most on that small block, not on the renamed file.
  • Platform.HOMEASSISTANT enum literal — used as a dict key throughout the codebase; removing it is a separate refactor with no real benefit (per the established migration playbook).
  • tools/homeassistant_tool.py — the HA REST/WS toolset that lets the agent call HA services from a prompt. It is a separate concern from this messaging adapter and is not affected.

What does not need a hook

  • setup_fn — Home Assistant has no _setup_homeassistant function in hermes_cli/setup.py and is not in hermes_cli/gateway.py::_builtin_setup_fn(). Setup runs through the existing toolset wizard in hermes_cli/tools_config.py, which is unchanged by this PR.
  • apply_yaml_config_fn — no homeassistant: block currently exists in config.yaml; there is nothing to translate.
  • cron_deliver_env_var — Home Assistant has no notion of a "home channel" the way Discord / Slack / Mattermost do. Cron deliver=homeassistant still works via standalone_sender_fn; it just always requires an explicit chat_id (the HA notify target, e.g. mobile_app_pixel_8).
  • allowed_users_env / allow_all_env — the HA adapter consumes the WebSocket event bus, not user-to-user messages; auth is per-token, not per-user.

Why _standalone_send uses notify.notify, not persistent_notification.create

The live adapter's send() calls persistent_notification.create (a notification visible in the HA UI). The legacy _send_homeassistant helper in tools/send_message_tool.py always used notify.notify (which triggers an actual HA notify service, e.g. push to a phone). Out-of-process cron delivery preserves the legacy helper's behavior — phones get pushed, not just an HA dashboard banner. The two surfaces were already different before this PR and this migration does not change that.

Verification

# Plugin discovery picks the migrated platform up
from hermes_cli.plugins import PluginManager
mgr = PluginManager(); mgr.discover_and_load(force=True)
from gateway.platform_registry import platform_registry
sorted(e.name for e in platform_registry.all_entries())
# → ['discord', 'google_chat', 'homeassistant', 'irc', 'line',
#    'mattermost', 'ntfy', 'simplex', 'teams']
e = platform_registry.get('homeassistant')
# e.source == 'plugin', e.standalone_sender_fn is not None,
# e.required_env == ['HASS_TOKEN'], e.emoji == '🏠'

Tests

  • tests/gateway/test_homeassistant.py — 41 tests, import paths updated to plugins.platforms.homeassistant.adapter, all mock.patch(...) strings updated. Passes.
  • tests/integration/test_ha_integration.py — adapter import path updated. Passes.
  • tests/tools/test_send_message_missing_platforms.py — the legacy (token, extra, chat_id, message)-shaped _send_homeassistant is preserved via a small SimpleNamespace shim that wraps the plugin's _standalone_send(pconfig, chat_id, message). This mirrors the shim mattermost added when it migrated, so the existing 4 test bodies don't have to be rewritten. Passes.

Focused suite: 64 passed.

Broader gateway + cron sweep: 6218 passed, 10 failures — identical set to the same sweep run against unmodified main (telegram approval/model-picker xdist isolation flakes, wecom_callback defusedxml issue, cron/test_cron_script::test_script_timeout subprocess-timeout fixture issue). Zero net new failures.

Context for in-flight HA PRs

There are several open HA PRs that touch gateway/platforms/homeassistant.py (#29199, #29388, #31389, #27270, #27272, #24053, #23643). After this migration the file is at plugins/platforms/homeassistant/adapter.py — git rename detection should make rebases largely mechanical, and the env-to-PlatformConfig block they care about stays in gateway/config.py. Happy to help with any rebase.

Move gateway/platforms/homeassistant.py into plugins/platforms/homeassistant/
following the same shape as the Mattermost and Discord migrations.

  - Adapter file is renamed via git mv (history is preserved).
  - register() exposes the platform via the plugin system instead of the
    hardcoded Platform.HOMEASSISTANT elif in gateway/run.py::build_adapter().
  - _standalone_send() replaces the legacy _send_homeassistant() helper in
    tools/send_message_tool.py.  Out-of-process cron delivery
    (deliver=homeassistant from a cron process not co-located with the
    gateway) now flows through the registry's standalone_sender_fn path
    instead of the hardcoded elif.
  - _is_connected() probes HASS_TOKEN via hermes_cli.gateway.get_env_value
    so existing connected-platform checks behave identically.

The HASS_TOKEN / HASS_URL env-to-PlatformConfig seeding in
gateway/config.py stays in core — same pattern bluebubbles, mattermost,
and discord migrations followed.  No setup_fn or apply_yaml_config_fn is
registered because Home Assistant has no _setup_homeassistant wizard in
hermes_cli/setup.py and no homeassistant: YAML block in config.yaml today;
setup runs through the existing hermes_cli/tools_config.py toolset wizard.

Test imports were rewritten across tests/gateway/test_homeassistant.py,
tests/integration/test_ha_integration.py, and
tests/tools/test_send_message_missing_platforms.py; the legacy
(token, extra, chat_id, message)-shaped _send_homeassistant call site is
preserved via a small SimpleNamespace shim in
test_send_message_missing_platforms.py (same approach used when
mattermost moved).

  - Focused HA suites (64 tests across the three rewritten files) pass.
  - Broader gateway/cron sweep produces 10 failures identical to main
    baseline (telegram approval/model-picker xdist isolation flakes,
    wecom_callback defusedxml issue, cron script_timeout fixture issue).
    Zero net new failures.
@kshitijk4poor kshitijk4poor force-pushed the feat/homeassistant-platform-plugin branch from 2c45b72 to 4bbdf57 Compare May 26, 2026 08:36
@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins labels May 26, 2026
@teknium1

teknium1 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Merged via #40709 — your commit was cherry-picked onto current main with your authorship preserved in git log (the original branch was 1211 commits behind). Thanks for the clean migration; it followed the mattermost/discord plugin shape exactly.

@teknium1 teknium1 closed this Jun 6, 2026
teknium1 pushed a commit that referenced this pull request Jun 6, 2026
`gateway/run.py::_UPDATE_ALLOWED_PLATFORMS` was a hardcoded frozenset
listing every messaging platform allowed to invoke the `/update` slash
command.  Plugin-migrated platforms (currently Discord and Mattermost,
soon also Home Assistant via #32500) declare `allow_update_command=True`
on their `PlatformEntry`, and `_handle_update_command` already falls
back to the registry when a platform isn't in the frozenset.  The result
was a silent redundancy: those entries said "allowed" twice, and the
registry flag was a no-op for them in practice.

  - Removed `Platform.DISCORD` and `Platform.MATTERMOST` from the frozenset.
  - Updated the docstring to make the split explicit (built-ins live in
    the frozenset; plugins use `allow_update_command` on the registry entry).

The remaining frozenset entries are all still built-in platforms living
under `gateway/platforms/` today.  Future plugin migrations should drop
their entry from the frozenset as part of the migration PR (or in a
sibling chore PR like this one).

Added a `TestUpdateCommandPlatformGate` test class that pins down all
three branches of the gate so future changes don't silently regress:

  - Programmatic interfaces (`Platform.WEBHOOK`, `Platform.API_SERVER`)
    must remain blocked.
  - Plugin-migrated platforms (Discord, Mattermost) must pass via the
    registry fallback.
  - Built-in platforms in the hardcoded frozenset (Telegram) must
    still pass without needing the registry.

The gate previously had zero direct test coverage — its only existing
coverage was `test_no_adapter_for_platform` which exercised a different
code path.
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
`gateway/run.py::_UPDATE_ALLOWED_PLATFORMS` was a hardcoded frozenset
listing every messaging platform allowed to invoke the `/update` slash
command.  Plugin-migrated platforms (currently Discord and Mattermost,
soon also Home Assistant via NousResearch#32500) declare `allow_update_command=True`
on their `PlatformEntry`, and `_handle_update_command` already falls
back to the registry when a platform isn't in the frozenset.  The result
was a silent redundancy: those entries said "allowed" twice, and the
registry flag was a no-op for them in practice.

  - Removed `Platform.DISCORD` and `Platform.MATTERMOST` from the frozenset.
  - Updated the docstring to make the split explicit (built-ins live in
    the frozenset; plugins use `allow_update_command` on the registry entry).

The remaining frozenset entries are all still built-in platforms living
under `gateway/platforms/` today.  Future plugin migrations should drop
their entry from the frozenset as part of the migration PR (or in a
sibling chore PR like this one).

Added a `TestUpdateCommandPlatformGate` test class that pins down all
three branches of the gate so future changes don't silently regress:

  - Programmatic interfaces (`Platform.WEBHOOK`, `Platform.API_SERVER`)
    must remain blocked.
  - Plugin-migrated platforms (Discord, Mattermost) must pass via the
    registry fallback.
  - Built-in platforms in the hardcoded frozenset (Telegram) must
    still pass without needing the registry.

The gate previously had zero direct test coverage — its only existing
coverage was `test_no_adapter_for_platform` which exercised a different
code path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants