commit 523732d4827a1e6d66d3ad40da1662f76db53da7 Author: Thomas Klaehn Date: Fri May 20 09:22:24 2022 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b3651c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +build/ +__pycache__/ +*.egg-info/ diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..5aac4a4 --- /dev/null +++ b/setup.py @@ -0,0 +1,56 @@ +"""Setup""" +import os +import shutil +import stat +from setuptools import setup +from setuptools.command.install import install + +NAME = 'Watercontrol' +VERSION = '1' +AUTHOR = 'Thomas Klaehn' +EMAIL = 'tkl@blackfinn.de' +PACKAGES = ['watercontrol'] +REQUIRES = ['RPi.GPIO'] +CONFIG_FILE = 'config.json' +PACKAGE_DATA = { + 'watercontrol': [ + 'watercontrol/config.json' + ] +} + +SERVICEDIR = "/lib/systemd/system" +START_SCRIPT = 'watercontrol.service' +DAEMON_START_SCRIPT = os.path.join(SERVICEDIR, START_SCRIPT) +LOGFILE = "/var/log/watercontrol.log" + +ENTRY_POINTS = { + 'console_scripts': [ + 'watercontrol = watercontrol.main:main' + ] +} + + +class Install(install): + """Installer""" + def run(self): + install.run(self) + os.makedirs(SERVICEDIR, exist_ok=True) + shutil.copyfile(START_SCRIPT, DAEMON_START_SCRIPT) + os.chmod(DAEMON_START_SCRIPT, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) + + try: + open(LOGFILE, 'r', encoding="UTF-8") + except FileNotFoundError: + os.makedirs(os.path.dirname(LOGFILE), exist_ok=True) + open(LOGFILE, 'x', encoding="UTF-8") + 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, include_package_data=True, package_data=PACKAGE_DATA, zip_safe=False, + install_requires=REQUIRES, entry_points=ENTRY_POINTS, cmdclass={'install': Install}) diff --git a/watercontrol.service b/watercontrol.service new file mode 100644 index 0000000..8ac9821 --- /dev/null +++ b/watercontrol.service @@ -0,0 +1,11 @@ +[Unit] +Description=Watercontrol service +After=multi-user.target + +[Service] +Type=idle +ExecStart=/usr/local/bin/watercontrol + +[Install] +WantedBy=multi-user.target + diff --git a/watercontrol/__init__.py b/watercontrol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/watercontrol/config.json b/watercontrol/config.json new file mode 100644 index 0000000..11e860b --- /dev/null +++ b/watercontrol/config.json @@ -0,0 +1,36 @@ +{ + "hostname": "localhost", + "port": "64001", + "water": [ + { + "id": "1", + "autostate": false, + "pin": "27", + "times": [ + { + "on_time": "7:00", + "off_time": "7:20" + }, + { + "on_time": "19:00", + "off_time": "19:20" + } + ] + }, + { + "id": "2", + "autostate": false, + "pin": "17", + "times": [ + { + "on_time": "7:20", + "off_time": "7:40" + }, + { + "on_time": "19:20", + "off_time": "19:40" + } + ] + } + ] +} diff --git a/watercontrol/config.py b/watercontrol/config.py new file mode 100644 index 0000000..7e85a36 --- /dev/null +++ b/watercontrol/config.py @@ -0,0 +1,72 @@ +"""config Module""" + +import json +import os +import shutil + +class Config(): + """Config class""" + def __init__(self, config_file) -> None: + self.config_file = config_file + self.config = None + + try: + with open(self.config_file, "r", encoding="UTF-8") as handle: + self.config = json.load(handle) + except FileNotFoundError: + os.makedirs(os.path.dirname(self.config_file), exist_ok=True) + shutil.copyfile("hochbeet/config.json", self.config_file) + with open(self.config_file, "r", encoding="UTF-8") as handle: + self.config = json.load(handle) + + + def hostinfo(self): + """Deliver hostinfo""" + res = self.config["hostname"], int(self.config["port"]) + return res + + + def get_water_autostate(self, ident: int): + """Return water auto state of entry belonging to ident""" + for entry in self.config['water']: + if int(entry['id']) == ident: + return entry['autostate'] + return None + + + def get_water_times(self, ident: int): + """Return water times of entry belonging to ident""" + for entry in self.config['water']: + if int(entry['id']) == ident: + return entry['times'] + return None + + + def set_water_autostate(self, ident: str): + """Set water auto state of entry belonging to ident""" + for idx in range(len(self.config['water'])): + if self.config['water'][idx]['id'] == ident: + self.config['water'][idx]['autostate'] = True + with open(self.config_file, "w", encoding="UTF-8") as handle: + json.dump(self.config, handle) + return + + + def clear_water_autostate(self, ident: str): + """Clear water auto state of entry belonging to ident""" + for idx in range(len(self.config['water'])): + if self.config['water'][idx]['id'] == ident: + self.config['water'][idx]['autostate'] = False + with open(self.config_file, "w", encoding="UTF-8") as handle: + json.dump(self.config, handle) + return + + + def set_water_times(self, ident: str, times): + """Set water times of entry belonging to ident""" + for idx in range(len(self.config['water'])): + if self.config['water'][idx]['id'] == ident: + self.config['water'][idx]['times'] = times + with open(self.config_file, "w", encoding="UTF-8") as handle: + json.dump(self.config, handle) + return diff --git a/watercontrol/control.py b/watercontrol/control.py new file mode 100644 index 0000000..b3308e9 --- /dev/null +++ b/watercontrol/control.py @@ -0,0 +1,117 @@ +"""control module""" +import datetime +import json +import logging +import os +import shutil +import threading +import time + +import RPi.GPIO as GPIO + +class Control(threading.Thread): + """Control class""" + def __init__(self, configfile): + threading.Thread.__init__(self) + self.run_condition = True + self.config_file = configfile + self.config = None + self.log = logging.getLogger() + self.trigger_read_config = True + self.water_state = [] + + def reload_config(self): + """Reload the config""" + self.log.info("Reloading configuration triggered") + self.trigger_read_config = True + + def load_config(self): + """load the config""" + try: + with open(self.config_file, "r", encoding="UTF-8") as handle: + self.config = json.load(handle) + except FileNotFoundError: + # create default config + os.makedirs(os.path.dirname(self.config_file), exist_ok=True) + shutil.copyfile("config/config.json", self.config_file) + with open(self.config_file, "r", encoding="UTF-8") as handle: + self.config = json.load(handle) + for _ in range(len(self.config['water'])): + self.water_state.append(False) + # Configure all water pins + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + for entry in self.config['water']: + pin = int(entry['pin']) + GPIO.setup(pin, GPIO.OUT) + GPIO.output(pin, 1) + + def run(self): + while self.run_condition: + if self.trigger_read_config: + self.trigger_read_config = False + self.load_config() + + # handle water entries + water = self.config['water'] + water_index = 0 + for entry in water: + now = datetime.datetime.now() + if entry['autostate']: + idx = 0 + if int(now.hour) >= 12: + idx = 1 + on_time_pattern = entry['times'][idx]['on_time'] + on_time_pattern = on_time_pattern.split(':') + on_time = now.replace(hour=int(on_time_pattern[0]), + minute=int(on_time_pattern[1]), + second=0, + microsecond=0) + off_time_pattern = entry['times'][idx]['off_time'] + off_time_pattern = off_time_pattern.split(':') + off_time = now.replace(hour=int(off_time_pattern[0]), + minute=int(off_time_pattern[1]), + second=0, + microsecond=0) + pin = int(entry['pin']) + + if now > on_time and now <= off_time and not self.water_state[water_index]: + GPIO.output(pin, 0) + self.water_state[water_index] = True + self.log.info("water on") + elif now > off_time and self.water_state[water_index]: + GPIO.output(pin, 1) + self.water_state[water_index] = False + self.log.info("water off") + water_index += 1 + time.sleep(1) + + + def stop(self): + """Stop execution""" + self.run_condition = False + self.join() + + def get_current_water_state(self, ident: int): + """Get water state""" + if ident > 0 and ident <= len(self.water_state): + return self.water_state[ident -1] + return None + + def set_water_state(self, ident: str): + """Set water state""" + ident = int(ident) + if ident > 0 and ident <= len(self.water_state): + pin = int(self.config['water'][ident - 1]['pin']) + self.water_state[ident - 1] = True + self.log.info("water on by button") + GPIO.output(pin, 0) + + def clear_water_state(self, ident: str): + """Clear water state""" + ident = int(ident) + if ident > 0 and ident <= len(self.water_state): + pin = int(self.config['water'][ident - 1]['pin']) + self.water_state[ident - 1] = False + self.log.info("water off by button") + GPIO.output(pin, 1) diff --git a/watercontrol/main.py b/watercontrol/main.py new file mode 100644 index 0000000..bd32d01 --- /dev/null +++ b/watercontrol/main.py @@ -0,0 +1,27 @@ +"""Entry point""" +#!/usr/bin/env python + +import logging +import os +import sys +from . import remote + +LOG_LEVEL = logging.INFO +LOG_FILE = "/var/log/watercontrol.log" +LOG_FORMAT = "%(asctime)s %(levelname)s %(message)s" + +def main(): + """Entry point""" + logging.basicConfig(format=LOG_FORMAT, level=LOG_LEVEL, filename=LOG_FILE) + # logging.basicConfig(format=LOG_FORMAT, level=LOG_LEVEL) + + log = logging.getLogger() + config_file = os.path.join(os.path.expanduser('~'), ".config/watercontrol/config.json") + + log.info("Starting...") + server = remote.RemoteControl(config_file) + server.start() + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/watercontrol/remote.py b/watercontrol/remote.py new file mode 100644 index 0000000..fba3f3a --- /dev/null +++ b/watercontrol/remote.py @@ -0,0 +1,53 @@ +"""Remote module""" + +import logging + +from xmlrpc.server import SimpleXMLRPCServer +from . import config, control + + +class RemoteControl(): + """RemoteControl""" + def __init__(self, configfile): + self.log = logging.getLogger() + self.config = config.Config(configfile) + + host = (self.config.hostinfo()) + self.server = SimpleXMLRPCServer(host, allow_none=True) + self.control = control.Control(configfile) + + self.server.register_function(self.set_water_autostate, 'set_water_autostate') + self.server.register_function(self.clear_water_autostate, 'clear_water_autostate') + self.server.register_function(self.set_water_times, 'set_water_times') + + self.server.register_function(self.config.get_water_autostate, 'get_water_autostate') + self.server.register_function(self.config.get_water_times, 'get_water_times') + + self.server.register_function(self.control.set_water_state, 'set_water_state') + self.server.register_function(self.control.clear_water_state, 'clear_water_state') + + def set_water_times(self, ident: str, times): + """Set water times""" + self.config.set_water_times(ident, times) + self.control.reload_config() + + def set_water_autostate(self, ident: str): + """Set water auto state""" + self.config.set_water_autostate(ident) + self.control.reload_config() + + def clear_water_autostate(self, ident: str): + """Clear water auto state""" + self.config.clear_water_autostate(ident) + self.control.reload_config() + + + def start(self): + """Start remote server""" + self.log.info('Control-c to quit') + self.control.start() + self.server.serve_forever() + + self.log.info("Shutting down...") + self.control.stop() + self.log.info("...done. Exiting...")