Initial commit

This commit is contained in:
Thomas Klaehn 2022-05-20 09:22:24 +02:00
commit 523732d482
9 changed files with 376 additions and 0 deletions

4
.gitignore vendored Normal file
View File

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

56
setup.py Executable file
View File

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

11
watercontrol.service Normal file
View File

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

0
watercontrol/__init__.py Normal file
View File

36
watercontrol/config.json Normal file
View File

@ -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"
}
]
}
]
}

72
watercontrol/config.py Normal file
View File

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

117
watercontrol/control.py Normal file
View File

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

27
watercontrol/main.py Normal file
View File

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

53
watercontrol/remote.py Normal file
View File

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