"""Implementation of a solar water heater example (with dummy sensors). In this scenario, we have a solar water heater that includes a water temperature sensor on the output pipe of the heater. There is also an actuator which controls a bypass valve: if the actuator is ON, the hot water is redirected to a Spa, instead of going to the house. The spa is acting as a heat sink, taking up the extra heat. The Controller class below implements a state machine which looks at the data from the temperature sensor and turns on the bypass valve when the heated water is too hot. To avoid oscillations, we use the following logic: 1. If the running average of the temperature exceeds T_high, turn on the bypass 2. When the running average dips below T_low (where T_low0: self.history = self.history[new_start:] self.history.append(event) return SensorEvent(ts=event.ts, sensor_id=event.sensor_id, val=total/cnt) def __repr__(self): return 'RunningAvg(%s)' % self.history_interval class Controller(OutputThing): """Input sensor events and output actuator settings. """ def __init__(self): super().__init__() self.state = INITIAL self.completed = False def _make_event(self, val): return SensorEvent(ts=time.time(), sensor_id='Controller', val=val) def on_t_high_next(self, event): if self.state==NORMAL or self.state==INITIAL: self._dispatch_next(self._make_event("ON")) self.state = TOO_HOT def on_t_high_completed(self): if not self.completed: self._dispatch_completed() self.completed = True def on_t_high_error(self, e): pass def on_t_low_next(self, event): if self.state==TOO_HOT or self.state==INITIAL: self._dispatch_next(self._make_event("OFF")) self.state = NORMAL def on_t_low_completed(self): if not self.completed: self._dispatch_completed() self.completed = True def on_t_low_error(self, e): pass def on_between_next(self, x): if self.state==INITIAL: self.state = NORMAL self._dispatch_next(self._make_event("OFF")) else: pass # stay in current state def on_between_error(self, e): pass def on_between_completed(self): pass # don't want to pass this forward, as it will happen after the first item def __repr__(self): return 'Controller' class BypassValveActuator(InputThing): def on_next(self, x): if x.val=='ON': print("Turning ON!") elif x.val=='OFF': print("Turning OFF!") else: raise FatalError("Unexpected event value for actuator: %s" % x.val) def __repr__(self): return 'BypassValveActuator' # The values we will use for the sensor input_sequence = [i for i in range(T_low-2, T_high+4)] + \ [i for i in range(T_high+2, T_low-6, -2)] + \ [i for i in range(T_low-4, T_high+4, 2)] + \ [i for i in range(T_high+2, T_low-6, -2)] # Add some random noise to our sequence for i in range(len(input_sequence)): input_sequence[i] = round(random.gauss(input_sequence[i], 2), 1) def run_example(): sensor = SensorAsOutputThing(DummyTempSensor('temp-1', input_sequence)) sensor.output() # let us see the raw values dispatcher = sensor.transduce(RunningAvg(4))\ .passthrough(lambda evt: print("Running avg temp: %s" % round(evt.val, 2))) \ .dispatch([(lambda v: v[2]>=T_high, 't_high'), (lambda v: v[2]<=T_low, 't_low')]) controller = Controller() dispatcher.connect(controller, port_mapping=('t_high', 't_high')) dispatcher.connect(controller, port_mapping=('t_low', 't_low')) dispatcher.connect(controller, port_mapping=('default', 'between')) controller.connect(BypassValveActuator()) sensor.print_downstream() scheduler = Scheduler(asyncio.get_event_loop()) scheduler.schedule_periodic(sensor, 0.5) scheduler.run_forever() print("got to the end") if __name__ == '__main__': run_example()