Fix MultiValueDictKeyError when radio group is unselected in save_set…#366
Fix MultiValueDictKeyError when radio group is unselected in save_set…#366jamesmulcahy wants to merge 2 commits intoNSPManager:develfrom
Conversation
…tings Radio button groups (optimistic_mode, show_screensaver_inside/outside_temperature, turn_on_behavior) are absent from POST data when neither option is pre-selected (e.g. fresh install with no stored value). Use .get() with safe defaults instead of hard dict access to avoid MultiValueDictKeyError. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
After upgrading to the latest beta, I updated some settings in the UI, and hit save, and hit this error in Django, as it's assuming some fields are already set. |
tpanajott
left a comment
There was a problem hiding this comment.
Looks like a nice patch, thanks. Please see review for a few quick things before merging.
| set_setting_value( | ||
| name="show_screensaver_inside_temperature", | ||
| value=request.POST["show_screensaver_inside_temperature"], | ||
| value=request.POST.get("show_screensaver_inside_temperature", "False"), |
There was a problem hiding this comment.
We want to keep this to "True". We always want to show inside default as default.
| set_setting_value( | ||
| name="show_screensaver_outside_temperature", | ||
| value=request.POST["show_screensaver_outside_temperature"], | ||
| value=request.POST.get("show_screensaver_outside_temperature", "False"), |
There was a problem hiding this comment.
We want to keep this to "True". We always want to show outside default as default.
| set_setting_value( | ||
| name="optimistic_mode", value=request.POST["optimistic_mode"] == "optimistic" | ||
| name="optimistic_mode", | ||
| value=request.POST.get("optimistic_mode", "") == "optimistic", |
There was a problem hiding this comment.
We want to keep optimistic mode as the default. So, adding "optimistic" to be the default in case it was not found would seem appropriate. Or perhaps even better, checking of "optimistic_mode" in request.POST before trying to access it as it would then not change a value in case it was missing in the request but actually set in the stored settings.
|
Having thought a bit more about this i find it strange that this issue would pop up. Sure it needs to be fixed but it shouldn't happen. When the manager first starts it will go through the database and remove any setting that exists but is no longer used and will set the default value in case it does not exist. This functionality can be seen here: It seems there may be some issue with the check in the Django template if it should check a radio box or not. |
Thanks for calling this out -- I asked Claude to investigate and it seems to have found the issue. TL;DR is that there are two values stored in the written defaults, with different casings. This causes confusion. I've included the more detailed analysis from Claude below; and will push a PR shortly containing the fixes it proposed. Optimistic Mode Bug — Root Cause & Fix PlanPRWhat was reported
The defensive fix (already merged to branch)
Root cause 1 — Duplicate keys in
|
| Line | Key | DB name | Default |
|---|---|---|---|
| 186 | OPTIMISTIC_MODE | optimistic_mode | "false" |
| 187 | MQTT_WAIT_TIME | mqtt_wait_time | "0" |
| 230 | MQTT_WAIT_TIME | mqtt_wait_time | "1000" |
| 231 | OPTIMISTIC_MODE | optimistic_mode | "True" |
With unordered_map initialiser lists the first key wins; the second is silently
discarded. So populate_default_and_clean() writes "false" (not "True") and
"0" (not "1000") to the DB on a fresh install.
Confirmed intended defaults (per tpanajott PR review):
optimistic_mode→"True"(optimistic mode ON by default)mqtt_wait_time→"1000"show_screensaver_inside_temperature→"True"show_screensaver_outside_temperature→"True"
Fix: Remove lines 186–187 (the erroneous first entries) so the correct entries
at 230–231 are no longer shadowed.
Root cause 2 — Case mismatch between C++ default and Django template
The C++ default writes "false" (lowercase). The Django template checks:
{% if optimistic_mode == "True" %}checked{% endif %}
{% if optimistic_mode == "False" %}checked{% endif %}"false" matches neither condition, so no radio is pre-selected. A form submitted
with no radio selection omits that key from the POST body entirely — which is what
triggered the KeyError.
Django saves booleans via str(True/False) = "True"/"False" (title case), so
once the user saves successfully the value is correct. The problem only surfaces when
the C++ default is the only value ever stored.
Fix: Fixing root cause 1 (correct default "True") resolves this for optimistic_mode.
Additionally, make the template comparisons case-insensitive using the |lower filter
so any future casing drift is harmless.
Root cause 3 — Wrong .get() defaults in views.py
The existing branch fix used:
request.POST.get("show_screensaver_inside_temperature", "False")
request.POST.get("show_screensaver_outside_temperature", "False")But per tpanajott's review, both should default to "True".
For optimistic_mode, tpanajott prefers skip the update entirely if the key is
absent from POST (rather than defaulting to a value), so the stored setting is not
accidentally overwritten.
Fixes summary
-
mqtt_manager_config.hpplines 186–187: Remove the duplicate/wrong entries so
the correct defaults on lines 230–231 take effect. -
views.pysave_settings: Fix.get()defaults for screensaver fields to"True";
guardoptimistic_modewithif "optimistic_mode" in request.POST. -
settings.html: Use|loweron template comparisons foroptimistic_modeto
be resilient to any future casing drift.
…tic_mode
The unordered_map had duplicate entries for OPTIMISTIC_MODE ("false") and
MQTT_WAIT_TIME ("0") earlier in the list, silently shadowing the correct
entries ("True" / "1000") that appeared later. Remove the early duplicates
so populate_default_and_clean() writes the intended defaults on fresh installs.
Also fix the screensaver temperature .get() fallbacks to "True" (matching the
C++ defaults and reviewer feedback), guard optimistic_mode with an `in` check
so a missing radio field never overwrites a stored value, and use the |lower
filter in the template comparisons to be resilient to future casing drift.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| // Map of SETTING key that points to a pair of two strings. Pair.first is database key as string, Pair.second is default value. | ||
| static inline std::unordered_map<MQTT_MANAGER_SETTING, std::pair<std::string, std::string>> _setting_key_map = { | ||
| {MQTT_MANAGER_SETTING::IS_HOME_ASSISTANT_ADDON, {"is_home_assistant_addon", "false"}}, | ||
| {MQTT_MANAGER_SETTING::OPTIMISTIC_MODE, {"optimistic_mode", "false"}}, |
There was a problem hiding this comment.
These are removed because they're duplicates -- they're also present further down
…tings
Radio button groups (optimistic_mode, show_screensaver_inside/outside_temperature, turn_on_behavior) are absent from POST data when neither option is pre-selected (e.g. fresh install with no stored value). Use .get() with safe defaults instead of hard dict access to avoid MultiValueDictKeyError.