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
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
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))
|
||||
Reference in New Issue
Block a user