Ground Control [main]
Initial commit
[1mdiff --git a/.gitignore b/.gitignore[m
[1mnew file mode 100644[m
[1mindex 0000000..b0d04ce[m
[1m--- /dev/null[m
[1m+++ b/.gitignore[m
[36m@@ -0,0 +1,3 @@[m
[32m+[m[32m__pycache__/[m
[32m+[m[32m*.swp[m
[32m+[m[32mground-control.cfg[m
[1mdiff --git a/LICENSE b/LICENSE[m
[1mnew file mode 100644[m
[1mindex 0000000..c939536[m
[1m--- /dev/null[m
[1m+++ b/LICENSE[m
[36m@@ -0,0 +1,21 @@[m
[32m+[m[32mMIT License[m
[32m+[m
[32m+[m[32mCopyright (c) 2023 Mike Cifelli[m
[32m+[m
[32m+[m[32mPermission is hereby granted, free of charge, to any person obtaining a copy[m
[32m+[m[32mof this software and associated documentation files (the "Software"), to deal[m
[32m+[m[32min the Software without restriction, including without limitation the rights[m
[32m+[m[32mto use, copy, modify, merge, publish, distribute, sublicense, and/or sell[m
[32m+[m[32mcopies of the Software, and to permit persons to whom the Software is[m
[32m+[m[32mfurnished to do so, subject to the following conditions:[m
[32m+[m
[32m+[m[32mThe above copyright notice and this permission notice shall be included in all[m
[32m+[m[32mcopies or substantial portions of the Software.[m
[32m+[m
[32m+[m[32mTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR[m
[32m+[m[32mIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,[m
[32m+[m[32mFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE[m
[32m+[m[32mAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER[m
[32m+[m[32mLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,[m
[32m+[m[32mOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE[m
[32m+[m[32mSOFTWARE.[m
[1mdiff --git a/README.md b/README.md[m
[1mnew file mode 100644[m
[1mindex 0000000..049c774[m
[1m--- /dev/null[m
[1m+++ b/README.md[m
[36m@@ -0,0 +1,42 @@[m
[32m+[m[32m# ground-control[m
[32m+[m
[32m+[m[32mA Raspberry Pi project for coordinating actions between systems.[m
[32m+[m
[32m+[m[32m## Configuration[m
[32m+[m[32m```[m
[32m+[m[32mcp ground-control.sample.cfg ground-control.cfg[m
[32m+[m[32m```[m
[32m+[m
[32m+[m[32m## Installation[m
[32m+[m[32m```[m
[32m+[m[32msudo ./install[m
[32m+[m[32m```[m
[32m+[m
[32m+[m[32m## Output[m
[32m+[m[32mBy default output will be in `/var/log/syslog`.[m
[32m+[m[32mA separate log file can be used by creating `/etc/rsyslog.d/30-ground-control.conf` containing:[m
[32m+[m[32m```[m
[32m+[m[32mif $programname == 'ground-control' then /var/log/ground-control.log[m
[32m+[m[32m& stop[m
[32m+[m[32m```[m
[32m+[m[32mand then restart the rsyslog service:[m
[32m+[m[32m```[m
[32m+[m[32msudo systemctl restart rsyslog[m
[32m+[m[32m```[m
[32m+[m[32mThis log file can be rotated by creating `/etc/logrotate.d/ground-control` containing:[m
[32m+[m[32m```[m
[32m+[m[32m/var/log/ground-control.log[m
[32m+[m[32m{[m
[32m+[m[32m rotate 14[m
[32m+[m[32m daily[m
[32m+[m[32m create[m
[32m+[m[32m missingok[m
[32m+[m[32m notifempty[m
[32m+[m[32m compress[m
[32m+[m[32m delaycompress[m
[32m+[m[32m postrotate[m
[32m+[m[32m /usr/lib/rsyslog/rsyslog-rotate[m
[32m+[m[32m endscript[m
[32m+[m[32m}[m
[32m+[m
[32m+[m[32m```[m
[1mdiff --git a/ground-control.py b/ground-control.py[m
[1mnew file mode 100644[m
[1mindex 0000000..ecade2b[m
[1m--- /dev/null[m
[1m+++ b/ground-control.py[m
[36m@@ -0,0 +1,91 @@[m
[32m+[m[32mimport json[m
[32m+[m[32mimport requests[m
[32m+[m
[32m+[m[32mfrom configparser import ConfigParser[m
[32m+[m[32mfrom signal import signal[m
[32m+[m[32mfrom signal import SIGTERM[m
[32m+[m[32mfrom time import sleep[m
[32m+[m
[32m+[m[32mconfig = ConfigParser()[m
[32m+[m[32mconfig.read('ground-control.cfg')[m
[32m+[m
[32m+[m[32mgarage_host = config['systems'].get('garage')[m
[32m+[m[32mmarshaller_host = config['systems'].get('marshaller')[m
[32m+[m
[32m+[m[32mntfy = config['ntfy'].get('host')[m
[32m+[m[32mtopic = config['ntfy'].get('topic')[m
[32m+[m[32mauth = config['ntfy'].get('auth')[m
[32m+[m
[32m+[m[32mntfy_uri = f'https://{ntfy}/{topic}/json?auth={auth}'[m
[32m+[m[32mgarage_uri = f'https://{garage_host}'[m
[32m+[m[32mmarshaller_uri = f'https://{marshaller_host}/on'[m
[32m+[m
[32m+[m[32mis_east_door_previously_open = False[m
[32m+[m
[32m+[m
[32m+[m[32mclass HaltException(Exception):[m
[32m+[m[32m pass[m
[32m+[m
[32m+[m
[32m+[m[32mdef main():[m
[32m+[m[32m signal(SIGTERM, raise_halt_exception)[m
[32m+[m
[32m+[m[32m isRunning = True[m
[32m+[m[32m isFirstRun = True[m
[32m+[m
[32m+[m[32m while isRunning:[m
[32m+[m[32m try:[m
[32m+[m[32m if isFirstRun:[m
[32m+[m[32m isFirstRun = False[m
[32m+[m[32m else:[m
[32m+[m[32m print('waiting to restart main loop', flush=True)[m
[32m+[m[32m sleep(15)[m
[32m+[m
[32m+[m[32m print('listening for events', flush=True)[m
[32m+[m[32m listen_for_notifications()[m
[32m+[m[32m except HaltException:[m
[32m+[m[32m isRunning = False[m
[32m+[m[32m except Exception as e:[m
[32m+[m[32m print(e, flush=True)[m
[32m+[m
[32m+[m
[32m+[m[32mdef listen_for_notifications():[m
[32m+[m[32m with requests.get(ntfy_uri, stream=True, timeout=60) as response:[m
[32m+[m[32m for line in response.iter_lines():[m
[32m+[m[32m if line:[m
[32m+[m[32m data = json.loads(line.decode('utf-8'))[m
[32m+[m
[32m+[m[32m if data['event'] == 'message' and is_east_door_newly_open():[m
[32m+[m[32m activate_marshaller()[m
[32m+[m
[32m+[m
[32m+[m[32mdef is_east_door_newly_open():[m
[32m+[m[32m global is_east_door_previously_open[m
[32m+[m
[32m+[m[32m is_door_open_now = is_east_door_currently_open()[m
[32m+[m
[32m+[m[32m if is_door_open_now != is_east_door_previously_open:[m
[32m+[m[32m is_east_door_previously_open = is_door_open_now[m
[32m+[m
[32m+[m[32m return is_door_open_now[m
[32m+[m
[32m+[m[32m return False[m
[32m+[m
[32m+[m
[32m+[m[32mdef is_east_door_currently_open():[m
[32m+[m[32m return requests.get(garage_uri, timeout=6).json()['east-door'] == 'opened'[m
[32m+[m
[32m+[m
[32m+[m[32mdef activate_marshaller():[m
[32m+[m[32m print('activating marshaller', flush=True)[m
[32m+[m[32m requests.get(marshaller_uri, timeout=1)[m
[32m+[m[32m requests.get(marshaller_uri, timeout=1)[m
[32m+[m[32m requests.get(marshaller_uri, timeout=1)[m
[32m+[m
[32m+[m
[32m+[m[32mdef raise_halt_exception(signum, frame):[m
[32m+[m[32m raise HaltException()[m
[32m+[m
[32m+[m
[32m+[m[32mif __name__ == '__main__':[m
[32m+[m[32m main()[m
[1mdiff --git a/ground-control.sample.cfg b/ground-control.sample.cfg[m
[1mnew file mode 100644[m
[1mindex 0000000..c0f79e7[m
[1m--- /dev/null[m
[1m+++ b/ground-control.sample.cfg[m
[36m@@ -0,0 +1,8 @@[m
[32m+[m[32m[systems][m
[32m+[m[32mgarage=localhost[m
[32m+[m[32mmarshaller=localhost[m
[32m+[m
[32m+[m[32m[ntfy][m
[32m+[m[32mhost=ntfy.sh[m
[32m+[m[32mtopic=topic[m
[32m+[m[32mauth=auth[m
[1mdiff --git a/ground-control.service b/ground-control.service[m
[1mnew file mode 100644[m
[1mindex 0000000..3d1253b[m
[1m--- /dev/null[m
[1m+++ b/ground-control.service[m
[36m@@ -0,0 +1,14 @@[m
[32m+[m[32m[Unit][m
[32m+[m[32mDescription=Ground Control[m
[32m+[m[32mAfter=multi.user.target[m
[32m+[m
[32m+[m[32m[Service][m
[32m+[m[32mType=simple[m
[32m+[m[32mWorkingDirectory=$workingDirectory[m
[32m+[m[32mExecStart=$execStart[m
[32m+[m[32mRestart=on-failure[m
[32m+[m[32mSyslogIdentifier=ground-control[m
[32m+[m[32mUser=$user[m
[32m+[m
[32m+[m[32m[Install][m
[32m+[m[32mWantedBy=multi-user.target[m
[1mdiff --git a/install b/install[m
[1mnew file mode 100755[m
[1mindex 0000000..27c9fbc[m
[1m--- /dev/null[m
[1m+++ b/install[m
[36m@@ -0,0 +1,36 @@[m
[32m+[m[32m#! /usr/bin/env python3[m
[32m+[m
[32m+[m[32mimport os[m
[32m+[m[32mimport sys[m
[32m+[m
[32m+[m[32mfrom string import Template[m
[32m+[m[32mfrom subprocess import check_call[m
[32m+[m[32mfrom subprocess import check_output[m
[32m+[m
[32m+[m[32mEXEC = 'ground-control.py'[m
[32m+[m[32mSERVICE = 'ground-control.service'[m
[32m+[m[32mSYSTEM_DIR = '/etc/systemd/system'[m
[32m+[m
[32m+[m[32mCURRENT_DIR = os.path.dirname(os.path.realpath(__file__))[m
[32m+[m[32mSERVICE_TEMPLATE = os.path.join(CURRENT_DIR, SERVICE)[m
[32m+[m[32mSERVICE_FILE = os.path.join(SYSTEM_DIR, SERVICE)[m
[32m+[m[32mPYTHON = sys.executable[m
[32m+[m[32mEXEC_START = f'{PYTHON} {EXEC}'[m
[32m+[m[32mUSER = check_output(['logname']).decode('utf-8').strip()[m
[32m+[m
[32m+[m[32mwith open(SERVICE_TEMPLATE) as f:[m
[32m+[m[32m serviceTemplate = Template(f.read())[m
[32m+[m
[32m+[m[32mserviceFile = serviceTemplate.substitute([m
[32m+[m[32m workingDirectory=CURRENT_DIR,[m
[32m+[m[32m execStart=EXEC_START,[m
[32m+[m[32m user=USER[m
[32m+[m[32m)[m
[32m+[m
[32m+[m[32mwith open(SERVICE_FILE, 'w') as f:[m
[32m+[m[32m f.write(serviceFile)[m
[32m+[m
[32m+[m[32mcheck_call(['systemctl', 'daemon-reload'])[m
[32m+[m[32mcheck_call(['systemctl', 'enable', '--no-pager', SERVICE])[m
[32m+[m[32mcheck_call(['systemctl', 'restart', '--no-pager', SERVICE])[m
[32m+[m[32mcheck_call(['systemctl', 'status', '--no-pager', SERVICE])[m