Logs load/start/pause/continue/stop/entering-step/wait-user/finished on Sud (components/sud.py) and connected/disconnected on StirrerTask/HeaterTask, using the self.log added earlier. SudForecastEstimator.estimate() (components/sud_forecast.py) drives its own throwaway Sud through an entire schedule in a tight loop (no real waiting) to predict its duration, and re-runs that on every real step transition too - since it's still a Sud, it logged through the exact same "Sud" logger as the real, server-driven one, making an instant internal forecast run indistinguishable in the log from an actual brew. Gave it its own "SudForecastEstimator" logger, silenced to WARNING by default, so only the real Sud's transitions show up.
102 lines
3.6 KiB
Python
102 lines
3.6 KiB
Python
import asyncio
|
|
from tasks import ATask, fire_and_forget
|
|
from ws.message import MsgIo
|
|
from components import AStirrer
|
|
from utils.value import ChangedInteger
|
|
|
|
|
|
class StirrerTask(ATask):
|
|
def __init__(self, stirrer_device: AStirrer, interval, msg_handler: MsgIo):
|
|
ATask.__init__(self, interval)
|
|
self.msg_handler = msg_handler
|
|
msg_handler.set_recv_handler(self.recv)
|
|
self.device = stirrer_device
|
|
self._on_connected_changed = None
|
|
stirrer_device.set_on_changed("speed", self.on_speed_changed)
|
|
stirrer_device.set_on_changed("dutyCycle", self.on_dutycycle_changed)
|
|
stirrer_device.set_on_changed("cycleTime", self.on_cycletime_changed)
|
|
stirrer_device.set_on_changed("connected", self.on_connected_changed)
|
|
|
|
def on_speed_changed(self, value):
|
|
asyncio.create_task(self.send({'Speed': value}))
|
|
|
|
def on_dutycycle_changed(self, value):
|
|
asyncio.create_task(self.send({'DutyCycle': value}))
|
|
|
|
def on_cycletime_changed(self, value):
|
|
asyncio.create_task(self.send({'CycleTime': value}))
|
|
|
|
def set_on_connected_changed(self, callback):
|
|
"""Registers a callback(connected: bool) invoked whenever the
|
|
device's connection state changes - lets server/brewpi.py wire this
|
|
into SudTask's start-gating/mid-brew auto-stop."""
|
|
self._on_connected_changed = callback
|
|
|
|
def on_connected_changed(self, value):
|
|
self.log.info("Connected" if value else "Disconnected")
|
|
fire_and_forget(self.send({'Connected': value}))
|
|
if self._on_connected_changed:
|
|
self._on_connected_changed(value)
|
|
|
|
def disconnect(self):
|
|
"""Disconnects the device and zeroes its speed setpoint, so a later
|
|
reconnect never resumes spinning at whatever speed was last set.
|
|
Used for every disconnect - a user-requested Disconnect, a
|
|
comm-error timeout, and the unexpected-exception recovery path
|
|
alike - not just the explicit one, since self.device.speed
|
|
otherwise still holds the old value and the duty-cycle pulsing in
|
|
AStirrer.process() re-sends it on the next isOn transition."""
|
|
self.device.disconnect()
|
|
self.device.set_speed(0.0)
|
|
|
|
async def recv(self, data):
|
|
for pair in data.items():
|
|
if pair[0] == 'Connect':
|
|
if self.device.connect():
|
|
self.device.activate(True)
|
|
elif pair[0] == 'Disconnect':
|
|
self.device.activate(False)
|
|
self.disconnect()
|
|
elif 'Speed' in pair[0]:
|
|
self.device.set_speed(pair[1])
|
|
|
|
async def send(self, data):
|
|
await self.msg_handler.send(data)
|
|
|
|
async def on_process(self):
|
|
await self.send({'Simulated': self.device.simulated})
|
|
|
|
self.device.set_speed(0.0)
|
|
self.device.set_cycle_time(10.0)
|
|
self.device.set_duty_cycle(1.0)
|
|
|
|
# Auto-connect on startup - see HeaterTask.on_process()'s own
|
|
# comment for why this replaces the old constructor-side connect.
|
|
self.device.connect()
|
|
# Explicit initial push - see HeaterTask.on_process()'s own comment
|
|
# for why a simulated device's connect() alone doesn't fire the
|
|
# observer.
|
|
await self.send({'Connected': self.device.connected})
|
|
|
|
# Outer retry loop - see HeaterTask.on_process()'s own comment for
|
|
# why: nothing restarts a dead ATask, so an exception escaping this
|
|
# coroutine entirely would permanently stop this device from ever
|
|
# being process()'d again, even after a later successful reconnect.
|
|
while True:
|
|
try:
|
|
with self.device.open():
|
|
while True:
|
|
try:
|
|
self.device.process()
|
|
except Exception as e:
|
|
self.log.error(f"comm error, marking disconnected: {e}")
|
|
self.disconnect()
|
|
await asyncio.sleep(self.interval)
|
|
except Exception as e:
|
|
self.log.error(f"unexpected error, recovering: {e}")
|
|
try:
|
|
self.disconnect()
|
|
except Exception:
|
|
pass
|
|
await asyncio.sleep(self.interval)
|