With TempControllerBase(APid, TempControllerFsm), APid.is_holding() (abstract stub returning None) shadowed TempControllerFsm.is_holding() in the MRO, so tc.is_holding() always returned None. The forecast simulation never called sud.temp_reached() and always hit MAX_TICKS. Swapping to TempControllerBase(TempControllerFsm, APid) puts the concrete FSM methods before APid's abstract stubs in the MRO. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tqxrk8uj4M3w3d3eXm3xK8