Initial commit

This commit is contained in:
Thomas Klaehn 2021-04-19 07:57:29 +02:00
commit d30a15567b
13 changed files with 305 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.egg-info/
build/
dist/

43
config/_Config.py Normal file
View File

@ -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)

1
config/__init__.py Normal file
View File

@ -0,0 +1 @@
from ._Config import Config

81
control/_Control.py Normal file
View File

@ -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

1
control/__init__.py Normal file
View File

@ -0,0 +1 @@
from ._Control import Control

22
heat/_Heat.py Normal file
View File

@ -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

1
heat/__init__.py Normal file
View File

@ -0,0 +1 @@
from ._Heat import Heat

72
remotectrl/_RemoteCtrl.py Normal file
View File

@ -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...")

1
remotectrl/__init__.py Normal file
View File

@ -0,0 +1 @@
from ._RemoteCtrl import RemoteCtrl

10
saunacontrol.service Normal file
View File

@ -0,0 +1,10 @@
[Unit]
Description=Saunacontrol
After=multi-user.target
[Service]
Type=idle
ExecStart=/usr/local/bin/saunacontrol
[Install]
WantedBy=multi-user.target

0
saunacontrol/__init__.py Normal file
View File

26
saunacontrol/__main__.py Normal file
View File

@ -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())

44
setup.py Normal file
View File

@ -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
}
)