Source code for juham_automation.automation.watercirculator
from typing import Any
from typing_extensions import override
import json
from masterpiece.mqtt import MqttMsg
from juham_core import Juham
from juham_core.timeutils import timestamp
[docs]
class WaterCirculator(Juham):
"""Hot Water Circulation Automation
This system monitors motion sensor data to detect home occupancy.
- **When motion is detected**: The water circulator pump is activated, ensuring hot water is
instantly available when the tap is turned on.
- **When no motion is detected for a specified period (in seconds)**: The pump automatically
switches off to conserve energy.
Future improvement idea
------------------------
In cold countries, such as Finland, energy conservation during the winter season may not be a priority.
In this case, an additional temperature sensor measuring the outside temperature could be used to determine whether
the circulator should be switched off at all. The circulating water could potentially act as an additional heating radiator.
Points to consider
------------------
- Switching the pump on and off may affect its lifetime.
- Keeping the pump running with hot water could impact the lifespan of the pipes, potentially causing
corrosion due to constant hot water flow.
"""
uptime = 60 * 60 # one hour
min_temperature = 37
def __init__(self, name: str, temperature_sensor: str) -> None:
super().__init__(name)
# input topics
self.motion_topic = self.make_topic_name("motion") # motion detection
self.temperature_topic = self.make_topic_name(temperature_sensor)
# relay to be controlled
self.topic_power = self.make_topic_name("power")
# for the pump controlling logic
self.current_motion: bool = False
self.relay_started_ts: float = 0
self.water_temperature: float = 0
self.water_temperature_updated: float = 0
self.initialized = False
[docs]
@override
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
super().on_connect(client, userdata, flags, rc)
if rc == 0:
self.subscribe(self.motion_topic)
self.subscribe(self.temperature_topic)
# reset the relay to make sure the initial state matches the state of us
self.publish_relay_state(0)
[docs]
@override
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
if msg.topic == self.temperature_topic:
m = json.loads(msg.payload.decode())
self.on_temperature_sensor(m, timestamp())
elif msg.topic == self.motion_topic:
m = json.loads(msg.payload.decode())
self.on_motion_sensor(m, timestamp())
else:
super().on_message(client, userdata, msg)
[docs]
def on_temperature_sensor(self, m: dict[str, Any], ts_utc_now: float) -> None:
"""Handle message from the hot water pipe temperature sensor.
Records the temperature and updates the water_temperature_updated attribute.
Args:
m (dict): temperature reading from the hot water blump sensor
ts_utc_now (float): _current utc time
"""
self.water_temperature = m["temperature"]
self.water_temperature_updated = ts_utc_now
# self.info(
# f"Temperature of circulating water updated to {self.water_temperature} C"
# )
[docs]
def on_motion_sensor(self, m: dict[str, dict[str, Any]], ts_utc_now: float) -> None:
"""Control the water cirulator bump.
Given message from the motion sensor consider switching the
circulator bump on.
Args:
msg (dict): directionary holding motion sensor data
ts_utc_now (float): current time stamp
"""
sensor = m["sensor"]
vibration: bool = bool(m["vibration"])
motion: bool = bool(m["motion"])
if motion or vibration:
# self.debug(f"Life form detected in {sensor}")
# honey I'm home
if not self.current_motion:
if self.water_temperature > self.min_temperature:
self.publish_relay_state(0)
# self.debug(
# f"Circulator: motion detected but water warm already {self.water_temperature} > {self.min_temperature} C"
# )
else:
self.current_motion = True
self.relay_started_ts = ts_utc_now
self.publish_relay_state(1)
self.initialized = True
self.info(
f"Circulator pump started, will run for {int(self.uptime / 60)} minutes "
)
else:
self.publish_relay_state(1)
self.relay_started_ts = ts_utc_now
# self.debug(
# f"Circulator pump has been running for {int(ts_utc_now - self.relay_started_ts)/60} minutes",
# " ",
# )
else:
if self.current_motion or not self.initialized:
elapsed: float = ts_utc_now - self.relay_started_ts
if elapsed > self.uptime:
self.publish_relay_state(0)
self.info(
f"Circulator pump stopped, no motion in {int(elapsed/60)} minutes detected",
"",
)
self.current_motion = False
self.initialized = True
else:
self.publish_relay_state(1)
# self.debug(
# f"Circulator bump stop countdown {int(self.uptime - (ts_utc_now - self.relay_started_ts ))/60} min"
# )
else:
self.publish_relay_state(0)
# self.debug(
# f"Circulator bump off already, temperature {self.water_temperature} C",
# "",
# )
[docs]
def publish_relay_state(self, state: int) -> None:
"""Publish power status.
Args:
state (int): 1 for on, 0 for off, as defined by Juham 'power' topic
"""
heat = {"Unit": self.name, "Timestamp": timestamp(), "State": state}
self.publish(self.topic_power, json.dumps(heat), 1, False)