Ground Control [main]

Initial commit

9593d77e10dfc0a0f8bd7318418cb5da7db30ffe
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])