From d30a15567b577c28fbf0d0ae88e0cb33da581252 Mon Sep 17 00:00:00 2001 From: Thomas Klaehn Date: Mon, 19 Apr 2021 07:57:29 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 ++ config/_Config.py | 43 +++++++++++++++++++++ config/__init__.py | 1 + control/_Control.py | 81 +++++++++++++++++++++++++++++++++++++++ control/__init__.py | 1 + heat/_Heat.py | 22 +++++++++++ heat/__init__.py | 1 + remotectrl/_RemoteCtrl.py | 72 ++++++++++++++++++++++++++++++++++ remotectrl/__init__.py | 1 + saunacontrol.service | 10 +++++ saunacontrol/__init__.py | 0 saunacontrol/__main__.py | 26 +++++++++++++ setup.py | 44 +++++++++++++++++++++ 13 files changed, 305 insertions(+) create mode 100644 .gitignore create mode 100644 config/_Config.py create mode 100644 config/__init__.py create mode 100644 control/_Control.py create mode 100644 control/__init__.py create mode 100644 heat/_Heat.py create mode 100644 heat/__init__.py create mode 100644 remotectrl/_RemoteCtrl.py create mode 100644 remotectrl/__init__.py create mode 100644 saunacontrol.service create mode 100644 saunacontrol/__init__.py create mode 100644 saunacontrol/__main__.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ae5013 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.egg-info/ +build/ +dist/ diff --git a/config/_Config.py b/config/_Config.py new file mode 100644 index 0000000..e09500c --- /dev/null +++ b/config/_Config.py @@ -0,0 +1,43 @@ + +import os +import json + +class Config(): + def __init__(self): + self.__config_file = os.path.join(os.path.expanduser('~'), ".config/sauna/config.json") + self.__config = None + + try: + with open(self.__config_file, "r") as handle: + self.__config = json.load(handle) + except FileNotFoundError: + os.makedirs(os.path.dirname(self.__config_file), exist_ok=True) + self.__config = { + "target_temperature": "80", + "temperature_step": "5", + "max_temperature": "120", + "runtime": "2:30", + "heat_pin": "26" + } + with open(self.__config_file, "w") as handle: + json.dump(self.__config, handle) + + def target_temperature(self): + return self.__config['target_temperature'] + + def temperature_step(self): + res =self.__config['temperature_step'] + print(res) + return res + + def heat_pin(self): + return self.__config['heat_pin'] + + def runtime(self): + return self.__config['runtime'] + + def update_target_temperature(self, temperature: int): + if temperature <= int(self.__config['max_temperature']): + self.__config['target_temperature'] = str(temperature) + with open(self.__config_file, "w") as handle: + json.dump(self.__config, handle) diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..606cef2 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1 @@ +from ._Config import Config diff --git a/control/_Control.py b/control/_Control.py new file mode 100644 index 0000000..5c043db --- /dev/null +++ b/control/_Control.py @@ -0,0 +1,81 @@ + +import datetime +import logging +from threading import Thread +import time + +from w1thermsensor import W1ThermSensor + +import heat + +class Control(Thread): + def __init__(self, heat_pin: int): + super(Control, self).__init__() + self.__run_condition = True + self.__runtime = datetime.timedelta(hours=2) + self.__on_time = None + self.__off_time = datetime.datetime.now() + self.__runtime + self.__heat = heat.Heat(heat_pin) + self.__state = False + self.__target_temperature = 80.0 + self.__span_temperature = 1.5 + self.__sensor = W1ThermSensor() + self.__log = logging.getLogger() + + def run(self): + while self.__run_condition: + if self.__state: + # check for elapsed runtime + now = datetime.datetime.now() + if now > self.__off_time: + self.__state = False + self.__log.info("switch off after %s", self.__runtime) + continue + # check for regulating + temperature = float(self.__sensor.get_temperature()) + if self.__heat.state() and temperature > (self.__target_temperature + self.__span_temperature): + self.__heat.off() + self.__log.info("switch heat off for regulating") + self.__log.info("current temperature: %s", temperature) + else: + if not self.__heat.state() and temperature < (self.__target_temperature - self.__span_temperature): + self.__heat.on() + self.__log.info("switch heat on for regulating") + self.__log.info("current temperature: %s", temperature) + else: + if self.__heat.state(): + self.__heat.off() + time.sleep(0.8) + + def stop(self): + self.__run_condition = False + self.join() + + def set_runtime(self, runtime): + dtime = datetime.datetime.strptime(runtime, "%H:%M") + self.__runtime = datetime.timedelta(hours=dtime.hour, minutes=dtime.minute) + + def get_runtime(self): + return self.__runtime + + def set_target_temperature(self, target_temperature): + self.__target_temperature = float(target_temperature) + + def get_target_temperature(self): + return self.__target_temperature + + def switch_on(self): + self.__on_time = datetime.datetime.now() + self.__off_time = self.__on_time + self.__runtime + self.__state = True + + def switch_off(self): + self.__state = False + + def state(self): + return self.__state + + def time_to_switch_off(self): + if self.__state: + return self.__off_time - datetime.datetime.now() + return None diff --git a/control/__init__.py b/control/__init__.py new file mode 100644 index 0000000..0b4ef5b --- /dev/null +++ b/control/__init__.py @@ -0,0 +1 @@ +from ._Control import Control diff --git a/heat/_Heat.py b/heat/_Heat.py new file mode 100644 index 0000000..5b06ca7 --- /dev/null +++ b/heat/_Heat.py @@ -0,0 +1,22 @@ +import RPi.GPIO as GPIO + +class Heat(): + def __init__(self, pin): + self.__pin = pin + self.__state = False + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + GPIO.setup(pin, GPIO.OUT) + if GPIO.input(pin): + self.__state = True + + def on(self): + self.__state = True + GPIO.output(self.__pin, 1) + + def off(self): + self.__state = False + GPIO.output(self.__pin, 0) + + def state(self): + return self.__state diff --git a/heat/__init__.py b/heat/__init__.py new file mode 100644 index 0000000..854537c --- /dev/null +++ b/heat/__init__.py @@ -0,0 +1 @@ +from ._Heat import Heat diff --git a/remotectrl/_RemoteCtrl.py b/remotectrl/_RemoteCtrl.py new file mode 100644 index 0000000..a9b57d0 --- /dev/null +++ b/remotectrl/_RemoteCtrl.py @@ -0,0 +1,72 @@ +import logging +from xmlrpc.server import SimpleXMLRPCServer + +import config +import control + +class RemoteCtrl: + def __init__(self, host="localhost", port=64001): + self.__log = logging.getLogger() + self.__server = SimpleXMLRPCServer((host, port), allow_none=True) + self.__server.register_function(self.set_runtime, 'set_runtime') + self.__server.register_function(self.get_runtime, 'get_runtime') + self.__server.register_function(self.get_target_temperature, 'get_target_temperature') + self.__server.register_function(self.get_temperature_step, 'get_temperature_step') + self.__server.register_function(self.set_target_temperature, 'set_target_temperature') + self.__server.register_function(self.switch_on, 'switch_on') + self.__server.register_function(self.switch_off, 'switch_off') + self.__server.register_function(self.state, 'state') + self.__server.register_function(self.time_to_switch_off, 'time_to_switch_off') + + self.__config = config.Config() + self.__ctrl = control.Control(int(self.__config.heat_pin())) + self.__ctrl.set_target_temperature(self.__config.target_temperature()) + self.__ctrl.set_runtime(self.__config.runtime()) + + + def set_runtime(self, runtime): + self.__ctrl.set_runtime(runtime) + + + def get_runtime(self): + return str(self.__ctrl.get_runtime) + + + def get_target_temperature(self): + return self.__ctrl.get_target_temperature() + + + def get_temperature_step(self): + return self.__config.temperature_step() + + + def set_target_temperature(self, target_temperature): + self.__ctrl.set_target_temperature(target_temperature) + self.__config.update_target_temperature(target_temperature) + + + def switch_on(self): + self.__ctrl.switch_on() + + + def switch_off(self): + self.__ctrl.switch_off() + + + def state(self): + return self.__ctrl.state() + + + def time_to_switch_off(self): + return str(self.__ctrl.time_to_switch_off()) + + + def start(self): + self.__log.info('Control-c to quit') + self.__ctrl.start() + self.__server.serve_forever() + + self.__log.info("Shutting down...") + self.__ctrl.stop() + self.switch_off() + self.__log.info("...done. Exiting...") diff --git a/remotectrl/__init__.py b/remotectrl/__init__.py new file mode 100644 index 0000000..7d8f6be --- /dev/null +++ b/remotectrl/__init__.py @@ -0,0 +1 @@ +from ._RemoteCtrl import RemoteCtrl diff --git a/saunacontrol.service b/saunacontrol.service new file mode 100644 index 0000000..913b9b3 --- /dev/null +++ b/saunacontrol.service @@ -0,0 +1,10 @@ +[Unit] +Description=Saunacontrol +After=multi-user.target + +[Service] +Type=idle +ExecStart=/usr/local/bin/saunacontrol + +[Install] +WantedBy=multi-user.target diff --git a/saunacontrol/__init__.py b/saunacontrol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/saunacontrol/__main__.py b/saunacontrol/__main__.py new file mode 100644 index 0000000..7c0ad4e --- /dev/null +++ b/saunacontrol/__main__.py @@ -0,0 +1,26 @@ + +import logging +import time +import sys + +import remotectrl + +LOG_LEVEL = logging.INFO +LOG_FILE = "/var/log/sauna.log" +LOG_FORMAT = "%(asctime)s %(levelname)s %(message)s" + + +def main(): + logging.basicConfig(format=LOG_FORMAT, level=LOG_LEVEL, filename=LOG_FILE) + # logging.basicConfig(format=LOG_FORMAT, level=LOG_LEVEL) + + log = logging.getLogger() + + log.info("Starting...") + server = remotectrl.RemoteCtrl() + server.start() + + server.stop() + +if __name__ == "__main__": + sys.exit(main()) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2a283b7 --- /dev/null +++ b/setup.py @@ -0,0 +1,44 @@ +import os +import shutil +import stat +from setuptools import setup +from setuptools.command.install import install + +NAME = 'Sauna control' +VERSION = '1' +AUTHOR = 'Thomas Klaehn' +EMAIL = 'tkl@blackfinn.de' +PACKAGES = ['config', 'control', 'heat', 'remotectrl', 'saunacontrol'] +REQUIRES = ['w1thermsensor', 'RPi.GPIO'] + +SERVICEDIR = "/lib/systemd/system" +DAEMON_START_SCRIPT = os.path.join(SERVICEDIR, 'saunacontrol.service') + +LOGFILE = "/var/log/sauna.log" + +ENTRY_POINTS = { + 'console_scripts': [ + 'saunacontrol = saunacontrol.__main__:main' + ] +} + +class Install(install): + def run(self): + install.run(self) + os.makedirs(SERVICEDIR, exist_ok=True) + shutil.copyfile('saunacontrol.service', os.path.join(SERVICEDIR, DAEMON_START_SCRIPT)) + os.chmod(DAEMON_START_SCRIPT, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) + + try: + open(LOGFILE, 'r') + except FileNotFoundError: + os.makedirs(os.path.dirname(LOGFILE), exist_ok=True) + open(LOGFILE, 'x') + os.chmod(LOGFILE, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH) + +setup(name=NAME, version=VERSION, long_description=__doc__, author=AUTHOR, author_email=EMAIL, + packages=PACKAGES, zip_safe=False, install_requires=REQUIRES, entry_points=ENTRY_POINTS, + cmdclass={ + 'install': Install + } + )