Skip to content

fix: prevent KeyError when action settings entity key is missing - #31

Open
brombomb wants to merge 3 commits into
gensyn:mainfrom
brombomb:entity-error
Open

fix: prevent KeyError when action settings entity key is missing#31
brombomb wants to merge 3 commits into
gensyn:mainfrom
brombomb:entity-error

Conversation

@brombomb

@brombomb brombomb commented Aug 5, 2026

Copy link
Copy Markdown

Real-World Situation & Crash Recovery Dynamics

This issue specifically occurs during StreamController crash recovery, ungraceful exits, or Flatpak sandbox state loss:

  1. In-Memory vs Disk Persistence: StreamController holds layout state in memory and flushes to disk (~/.var/app/com.core447.StreamController/data/pages/Home.json). If StreamController crashes, is force-killed, or experiences an unhandled exception before a clean shutdown, in-memory action settings are not written to disk, leaving the action entry with empty/unpopulated settings ({} or {"version": 1}).
  2. Multi-Action Key Cascade: On keys configured with multiple actions (e.g., separate ShowText actions for Top, Middle, and Bottom slots), a reconnect event fires multiple refresh() calls in rapid succession.
  3. The Exception Loop: When Home Assistant reconnects, home_assistant_backend.py iterates over active actions. When an un-persisted or partially-configured action is encountered, self.settings.get_entity() attempts settings['entity']['entity'], throwing KeyError: 'entity'. This unhandled exception breaks the callback loop, preventing subsequent actions from initializing cleanly and creating a persistent crash-restart loop.

Steps to Reproduce

  1. Add a Show Text or Perform Action button to a StreamController layout.
  2. Force-kill or crash StreamController before performing a clean application exit (so the page JSON on disk retains an empty or partially written settings object {}).
  3. Relaunch StreamController and trigger a Home Assistant reconnect/restart.
  4. Inspect StreamController.log: every background refresh pass for the recovered action raises KeyError: 'entity' and aborts subsequent action setups on that key.

Environment & System Info

  • OS: Ubuntu 26.04 LTS (Linux 7.0.0-28-generic x86_64)
  • Python Version: 3.14.4
  • StreamController Installation: Flatpak (com.core447.StreamController / stable)
  • HomeAssistantPlugin Version: v1.1.1

Proposed Fix

  1. Adds defensive _get_*_settings() helpers across BaseSettings, ShowTextSettings, ShowIconSettings, PerformActionSettings, CustomizationSettings, and LevelDialSettings to safely check if settings sections exist.
  2. Self-heals by populating default settings via deepcopy(DEFAULT_SETTINGS) and returning fallback/empty values ("") when settings keys are transiently missing during HA reconnects or resets.
  3. Adds regression unit test test_missing_entity_key in test_base_settings.py.

AI Disclosure

This pull request was diagnosed, developed, and verified with assistance from Google Antigravity (AGY). Antigravity was used to analyze stack traces, audit settings deserialization, develop defensive type-safe routines, author unit tests, and verify overall code correctness.

@gensyn

gensyn commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Hm, I regularly restart Home Assistant while the plugin is connected and it never failed to reconnect. I would like to understand and reproduce the problem before merging.

@brombomb

brombomb commented Aug 5, 2026

Copy link
Copy Markdown
Author

Again from Gemini, and this probably hints at my setup pretty well...

Step-by-Step Reproduction

  1. Add a new action: Add a "Show Text" or "Show Icon" action to a StreamController page.
  2. Leave it unconfigured: Do not select an entity (or create a button, select a domain, then clear it).
  3. Trigger a Home Assistant restart or disconnect: Restart Home Assistant or disconnect/reconnect the plugin backend.
  4. Check logs: Observe ~/.var/app/com.core447.StreamController/data/logs/StreamController.log (or terminal output). Every refresh cycle for the unconfigured button will raise:
    KeyError: 'entity'
    Traceback (most recent call last):
    File ".../actions/show_text/text_action.py", line 262, in refresh
    entity = self.settings.get_entity()
    File ".../actions/cores/base_core/base_settings.py", line 38, in get_entity
    return self._action.get_settings()[const.SETTING_ENTITY][const.SETTING_ENTITY]

Here is a sanitized version of my Home Screen Layout
Home.json

Keys in Home.json with Empty / Unset Entity IDs ('entity': '')

These keys have an active Home Assistant action with a domain assigned, but the target entity ID is empty (""). During a reconnect, refresh() attempts to inspect these keys:

• Key 0x0 (Action #0: HomeAssistantPlugin::ShowText)
• entity: {"domain": "ai_task", "entity": ""}
• Key 4x0 (Action #0: HomeAssistantPlugin::PerformAction & Action #1: HomeAssistantPlugin::ShowIcon)
• entity: {"domain": "script", "entity": ""} / {"domain": "ai_task", "entity": ""}
• Key 3x1 (Action #0: HomeAssistantPlugin::PerformAction)
• entity: {"domain": "ai_task", "entity": ""}
• Key 3x2 (Action #0: HomeAssistantPlugin::PerformAction)
• entity: {"domain": "ai_task", "entity": ""}
• Key 4x2 (Action #0: HomeAssistantPlugin::PerformAction & Action #1: HomeAssistantPlugin::ShowText)
• entity: {"domain": "ai_task", "entity": ""}

What's frustrating here, is I have configured many of these keys. For example the Key 0x0 should be:

image

All the keys except CPU and Weather are HA driven here (same Home screen)

image

I don't know if this specific issue is wiping my keys, or another crash is.

### System & Environment Information

- **OS / Linux Distro**: Ubuntu 26.04 LTS (`Linux 7.0.0-28-generic x86_64`)
- **Python Version**: `3.14.4`
- **StreamController Installation**: Flatpak (`com.core447.StreamController` / `stable`)
- **HomeAssistantPlugin Version**: `v1.1.1` (Manifest)

Why Your Button Configs Kept Resetting to ai_task

Here is the exact mechanism causing your buttons to reset:

  1. ai_task is Index 0: Home Assistant domains are sorted alphabetically (['ai_task', 'alarm_control_panel', 'automation', 'binary_sensor', ...]). ai_task is the very first domain in the list.
  2. GTK Dropdown Signal Firing on UI Re-render:
    • When StreamController starts up, recovers from a crash, or opens the button configuration panel, GTK initializes the ComboRow dropdown widget.
    • Before the dropdown loads your saved domain (e.g. switch), GTK's widget initialization temporarily defaults to index 0 (ai_task) with old_domain = None.
  3. The Reset Trigger:
    • In base_core.py, on_change_domain evaluates if old_domain != domain: (None != "ai_task").
    • Because None != "ai_task" evaluates to True, base_core.py runs self.settings.reset("ai_task").
    • This wipes out your saved entity (e.g., switch.toy_story) and overwrites the domain with ai_task.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants