From 9593d77e10dfc0a0f8bd7318418cb5da7db30ffe Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Mon, 30 Oct 2023 09:20:48 -0400 Subject: [PATCH] Initial commit --- .gitignore | 3 ++ LICENSE | 21 +++++++++ README.md | 42 ++++++++++++++++++ ground-control.py | 91 +++++++++++++++++++++++++++++++++++++++ ground-control.sample.cfg | 8 ++++ ground-control.service | 14 ++++++ install | 36 ++++++++++++++++ 7 files changed, 215 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 ground-control.py create mode 100644 ground-control.sample.cfg create mode 100644 ground-control.service create mode 100755 install diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0d04ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.swp +ground-control.cfg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c939536 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mike Cifelli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..049c774 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# ground-control + +A Raspberry Pi project for coordinating actions between systems. + +## Configuration +``` +cp ground-control.sample.cfg ground-control.cfg +``` + +## Installation +``` +sudo ./install +``` + +## Output +By default output will be in `/var/log/syslog`. +A separate log file can be used by creating `/etc/rsyslog.d/30-ground-control.conf` containing: +``` +if $programname == 'ground-control' then /var/log/ground-control.log +& stop +``` +and then restart the rsyslog service: +``` +sudo systemctl restart rsyslog +``` +This log file can be rotated by creating `/etc/logrotate.d/ground-control` containing: +``` +/var/log/ground-control.log +{ + rotate 14 + daily + create + missingok + notifempty + compress + delaycompress + postrotate + /usr/lib/rsyslog/rsyslog-rotate + endscript +} + +``` diff --git a/ground-control.py b/ground-control.py new file mode 100644 index 0000000..ecade2b --- /dev/null +++ b/ground-control.py @@ -0,0 +1,91 @@ +import json +import requests + +from configparser import ConfigParser +from signal import signal +from signal import SIGTERM +from time import sleep + +config = ConfigParser() +config.read('ground-control.cfg') + +garage_host = config['systems'].get('garage') +marshaller_host = config['systems'].get('marshaller') + +ntfy = config['ntfy'].get('host') +topic = config['ntfy'].get('topic') +auth = config['ntfy'].get('auth') + +ntfy_uri = f'https://{ntfy}/{topic}/json?auth={auth}' +garage_uri = f'https://{garage_host}' +marshaller_uri = f'https://{marshaller_host}/on' + +is_east_door_previously_open = False + + +class HaltException(Exception): + pass + + +def main(): + signal(SIGTERM, raise_halt_exception) + + isRunning = True + isFirstRun = True + + while isRunning: + try: + if isFirstRun: + isFirstRun = False + else: + print('waiting to restart main loop', flush=True) + sleep(15) + + print('listening for events', flush=True) + listen_for_notifications() + except HaltException: + isRunning = False + except Exception as e: + print(e, flush=True) + + +def listen_for_notifications(): + with requests.get(ntfy_uri, stream=True, timeout=60) as response: + for line in response.iter_lines(): + if line: + data = json.loads(line.decode('utf-8')) + + if data['event'] == 'message' and is_east_door_newly_open(): + activate_marshaller() + + +def is_east_door_newly_open(): + global is_east_door_previously_open + + is_door_open_now = is_east_door_currently_open() + + if is_door_open_now != is_east_door_previously_open: + is_east_door_previously_open = is_door_open_now + + return is_door_open_now + + return False + + +def is_east_door_currently_open(): + return requests.get(garage_uri, timeout=6).json()['east-door'] == 'opened' + + +def activate_marshaller(): + print('activating marshaller', flush=True) + requests.get(marshaller_uri, timeout=1) + requests.get(marshaller_uri, timeout=1) + requests.get(marshaller_uri, timeout=1) + + +def raise_halt_exception(signum, frame): + raise HaltException() + + +if __name__ == '__main__': + main() diff --git a/ground-control.sample.cfg b/ground-control.sample.cfg new file mode 100644 index 0000000..c0f79e7 --- /dev/null +++ b/ground-control.sample.cfg @@ -0,0 +1,8 @@ +[systems] +garage=localhost +marshaller=localhost + +[ntfy] +host=ntfy.sh +topic=topic +auth=auth diff --git a/ground-control.service b/ground-control.service new file mode 100644 index 0000000..3d1253b --- /dev/null +++ b/ground-control.service @@ -0,0 +1,14 @@ +[Unit] +Description=Ground Control +After=multi.user.target + +[Service] +Type=simple +WorkingDirectory=$workingDirectory +ExecStart=$execStart +Restart=on-failure +SyslogIdentifier=ground-control +User=$user + +[Install] +WantedBy=multi-user.target diff --git a/install b/install new file mode 100755 index 0000000..27c9fbc --- /dev/null +++ b/install @@ -0,0 +1,36 @@ +#! /usr/bin/env python3 + +import os +import sys + +from string import Template +from subprocess import check_call +from subprocess import check_output + +EXEC = 'ground-control.py' +SERVICE = 'ground-control.service' +SYSTEM_DIR = '/etc/systemd/system' + +CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) +SERVICE_TEMPLATE = os.path.join(CURRENT_DIR, SERVICE) +SERVICE_FILE = os.path.join(SYSTEM_DIR, SERVICE) +PYTHON = sys.executable +EXEC_START = f'{PYTHON} {EXEC}' +USER = check_output(['logname']).decode('utf-8').strip() + +with open(SERVICE_TEMPLATE) as f: + serviceTemplate = Template(f.read()) + +serviceFile = serviceTemplate.substitute( + workingDirectory=CURRENT_DIR, + execStart=EXEC_START, + user=USER +) + +with open(SERVICE_FILE, 'w') as f: + f.write(serviceFile) + +check_call(['systemctl', 'daemon-reload']) +check_call(['systemctl', 'enable', '--no-pager', SERVICE]) +check_call(['systemctl', 'restart', '--no-pager', SERVICE]) +check_call(['systemctl', 'status', '--no-pager', SERVICE])