Files
brewpi/utils/config_validate.py
T
jensandClaude Sonnet 5 0171cb20c7 config: fail fast on missing TempCtrl PID gains
Config access was unchecked dict[key] everywhere, so a missing/
misnamed gain (we've hit this once already: config.json.sim's gain/
Model nesting mismatch) surfaced as a bare KeyError from deep inside
some component's __init__ or first process() tick, with no indication
which config key was wrong.

utils/config_validate.py adds a small stdlib-only dotted-path checker
(no schema library - matches this project's stdlib-first leaning) and
validate_temp_ctrl(), scoped to the section that actually caused an
incident: TempCtrl.pid_type plus every kp/ki/kd/kt gain under
TempCtrl.Outer and TempCtrl.Inner.{Heat,Hold,Cool}. Called once in
server/brewpi.py right after json.load(), before any factory runs.
Optional-with-default keys (Outer.y_hold_min, Inner.Hold.yi_max,
Thresholds) stay optional here too.

Heater/Stirrer/TempSensor/Pot sections and unknown-key warnings are
deliberately left for later - see components/pid/TODO.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F4oEE99rkKVU8L8tkzXWdG
2026-07-11 12:33:57 +02:00

36 lines
1.8 KiB
Python

class ConfigError(Exception):
"""Raised for a missing or wrong-typed config key, named by its full
dotted path (e.g. "TempCtrl.Inner.Hold.kp") so the error points straight
at the offending line in config.json instead of surfacing as a bare
KeyError/TypeError from wherever the value first gets used."""
def require(config, path, type=None):
node = config
parts = path.split('.')
for i, key in enumerate(parts):
if not isinstance(node, dict) or key not in node:
raise ConfigError("missing required config key: {}".format('.'.join(parts[:i + 1])))
node = node[key]
if type is not None and not isinstance(node, type):
type_name = ' or '.join(t.__name__ for t in type) if isinstance(type, tuple) else type.__name__
raise ConfigError("config key {} must be {}, got {!r}".format(path, type_name, node))
return node
def validate_temp_ctrl(config):
"""Checked eagerly at startup so a missing/misnamed PID gain fails with
a clear message here instead of a bare KeyError mid-__init__ or on the
first process() tick (this bit us once already: config.json.sim's gain/
Model nesting mismatch). Only the gains every pid_type actually reads
unconditionally are required; anything with its own default elsewhere
(Outer.y_hold_min, Inner.Hold.yi_max, the whole Thresholds section -
see temp_controller_base.py/temp_controller_fsm.py's DEFAULT_THRESHOLDS)
stays optional here too, on purpose."""
require(config, 'TempCtrl', dict)
require(config, 'TempCtrl.pid_type', str)
for gain in ('kp', 'ki', 'kd', 'kt'):
require(config, 'TempCtrl.Outer.{}'.format(gain), (int, float))
for branch in ('Heat', 'Hold', 'Cool'):
require(config, 'TempCtrl.Inner.{}.{}'.format(branch, gain), (int, float))