Skip to content

fix: handle missing customizations key to prevent MediaPlayerThread crash#26

Open
seffyroff wants to merge 1 commit into
gensyn:mainfrom
seffyroff:fix/keyerror-customizations-missing-key
Open

fix: handle missing customizations key to prevent MediaPlayerThread crash#26
seffyroff wants to merge 1 commit into
gensyn:mainfrom
seffyroff:fix/keyerror-customizations-missing-key

Conversation

@seffyroff

Copy link
Copy Markdown

What

get_customizations() in CustomizationSettings used a bare dict subscript to read the customizations key:

self._action.get_settings()[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS]

When the customizations key is absent — which is the default state for any newly configured ShowText or other customization-based action — this raises a KeyError.

Why it matters

The exception isn't caught at the call site. It propagates up through on_ready() into MediaPlayerThread, killing the thread. Since MediaPlayerThread is responsible for rendering all button images on the deck, every dynamic button stops rendering — clock, CPU, weather, sensor values, all of it. Button presses still work (separate input thread) so the failure mode is subtle: the deck looks static but responds to presses, with no obvious error surfaced to the user.

This is reproducible with a fresh ShowText action that has never had a customization added to it.

Fix

Replace the bare subscript with .get(..., []) so a missing key returns an empty list instead of crashing:

self._action.get_settings()[self.customization_name].get(customization_const.SETTING_CUSTOMIZATIONS, [])

One-line change. Consistent with how the key would logically be treated if absent (no customizations configured = empty list).

Related

Similar MediaPlayerThread crash pattern to #5 and #19, but a distinct root cause in a different code path.

Using bare dict subscript raises KeyError when the 'customizations' key
is absent from the action's settings (the default state for a newly
configured ShowText or similar action). This exception propagates into
MediaPlayerThread, killing it — which silently stops all button
rendering on the deck while button presses continue to work.

Replace the bare subscript with .get(..., []) so a missing key is
treated as an empty list rather than a crash.
@gensyn

gensyn commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Hi, could you show me a crash log and a step-by-step guide on how to recreate the issue? I have not had an error with that key in the dict missing. Also, the current way of access is intentional because I want missing keys to throw an error as this should not happen - they should be initialized to not be missing.

@seffyroff

Copy link
Copy Markdown
Author

Hi, thanks for looking at this. Here's the crash log and the root cause analysis — I think there's a schema migration gap rather than the key being reliably initialised.

Crash log

2026-06-10 06:18:06.568 | ERROR | src.backend.DeckManagement.DeckController:run:78 - An error has been caught in function 'run', process 'MainProcess' (3), thread 'MediaPlayerThread' (140021369665216):
Traceback (most recent call last):
  ...
  File ".../DeckController.py", line 682, in load_input
    controller_input.load_from_input_dict(input_dict, update)
    │                                     └ {'states': {'0': {'labels': {'top': {'text': 'Office', 'font-size': 11}}, 'actions': [{'id': 'HomeAssistantPlugin::ShowText', ...

  File ".../customization_core.py", line 122, in set_enabled_disabled
    len(self.settings.get_customizations()) > 0

  File ".../customization_settings.py", line 22, in get_customizations
    self._action.get_settings()[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS]]
    │    │                       └ 'text'                └ 'customizations'

KeyError: 'customizations'

The action is a ShowText for a Home Assistant sensor that was fully configured (entity set, label showing). settings['text'] exists and is non-empty — but settings['text']['customizations'] is missing.

Why the key is absent

The initialisation in ShowTextSettings.__init__() (text_settings.py line 32) only writes DEFAULT_SETTINGS (which includes customizations: []) when the entire settings['text'] block is missing:

if not self._action.get_settings().get(text_const.SETTING_TEXT):
    settings[text_const.SETTING_TEXT] = DEFAULT_SETTINGS.copy()

For any action that was already configured with an entity before the customizations key was added to DEFAULT_SETTINGS, settings['text'] is non-empty and truthy, so this block is skipped. The new key is never back-filled into the existing settings dict. New actions get it; older saved actions don't.

Suggested fix

A setdefault after the conditional would handle both cases without clobbering existing settings:

if not self._action.get_settings().get(text_const.SETTING_TEXT):
    settings = self._action.get_settings()
    settings[text_const.SETTING_TEXT] = DEFAULT_SETTINGS.copy()
    self._action.set_settings(settings)

# Back-fill any keys added to DEFAULT_SETTINGS after initial save
settings = self._action.get_settings()
for key, default in DEFAULT_SETTINGS.items():
    settings[text_const.SETTING_TEXT].setdefault(key, default)
self._action.set_settings(settings)

Or more simply, just ensure customizations is always present:

settings[text_const.SETTING_TEXT].setdefault(customization_const.SETTING_CUSTOMIZATIONS, [])

The same pattern likely applies to ShowIconSettings and LevelDialSettings if they have the same conditional initialisation.

Happy to update this PR to fix the root cause in text_settings.py instead of (or as well as) the defensive .get() in get_customizations() — whichever you prefer. Let me know.

@gensyn

gensyn commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Hi, thanks for looking at this. Here's the crash log and the root cause analysis — I think there's a schema migration gap rather than the key being reliably initialised.

Crash log

2026-06-10 06:18:06.568 | ERROR | src.backend.DeckManagement.DeckController:run:78 - An error has been caught in function 'run', process 'MainProcess' (3), thread 'MediaPlayerThread' (140021369665216):
Traceback (most recent call last):
  ...
  File ".../DeckController.py", line 682, in load_input
    controller_input.load_from_input_dict(input_dict, update)
    │                                     └ {'states': {'0': {'labels': {'top': {'text': 'Office', 'font-size': 11}}, 'actions': [{'id': 'HomeAssistantPlugin::ShowText', ...

  File ".../customization_core.py", line 122, in set_enabled_disabled
    len(self.settings.get_customizations()) > 0

  File ".../customization_settings.py", line 22, in get_customizations
    self._action.get_settings()[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS]]
    │    │                       └ 'text'                └ 'customizations'

KeyError: 'customizations'

Could you please detail the steps you are doing to reproduce the issue?

The action is a ShowText for a Home Assistant sensor that was fully configured (entity set, label showing). settings['text'] exists and is non-empty — but settings['text']['customizations'] is missing.

Why the key is absent

The initialisation in ShowTextSettings.__init__() (text_settings.py line 32) only writes DEFAULT_SETTINGS (which includes customizations: []) when the entire settings['text'] block is missing:

if not self._action.get_settings().get(text_const.SETTING_TEXT):
    settings[text_const.SETTING_TEXT] = DEFAULT_SETTINGS.copy()

For any action that was already configured with an entity before the customizations key was added to DEFAULT_SETTINGS, settings['text'] is non-empty and truthy, so this block is skipped. The new key is never back-filled into the existing settings dict. New actions get it; older saved actions don't.

The customizations key was present from the very beginning of this plugin, so this should not be the issue. All the more reason to investigate where the error actually stems from.

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