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
36 lines
1.8 KiB
Python
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))
|