spacestated/spacestated
2024-06-19 21:43:59 +02:00

116 lines
4 KiB
Python
Executable file

#!/usr/bin/env python3
import hashlib
import os
import os.path as path
import subprocess
import time
import syslog
MQTT_HOST = 'mqtt.bitlair.nl'
HOOK_DIR = path.dirname(__file__)
RUN_DIR = '/tmp/spacestate'
def mqtt_get(topic, retry=3):
try:
cmd = ['/usr/local/bin/mqtt-simple', '--one', '-h', MQTT_HOST, '-s', topic]
return subprocess.check_output(cmd, timeout=1)[:-1].decode('utf8')
except subprocess.TimeoutExpired:
# Sometimes MQTT is derp. Try again just in case the message is retained.
if retry > 0:
return mqtt_get(topic, retry - 1)
raise Exception('Topic %s is not retained' % topic)
def hash_file(f):
with open(f, 'rb') as f:
digest = hashlib.sha1()
while True:
buf = f.read(4096)
if not buf:
break
digest.update(buf)
return digest.hexdigest()
def alarm_disarmed():
return mqtt_get('bitlair/alarm').split(' ')[0] == 'disarmed'
def getstate_djo():
return mqtt_get('bitlair/switch/state/djo') == 'ON' and alarm_disarmed()
def getstate_bitlair():
return mqtt_get('bitlair/switch/state/bitlair') == 'ON' and alarm_disarmed()
def get_state():
return {
'bitlair': getstate_bitlair(),
'djo': getstate_djo(),
}
def await_state_change(prev_state):
state = None
while True:
try:
state = get_state()
if state != prev_state:
return state
except Exception as err:
syslog.syslog(syslog.LOG_ERR, str(err))
time.sleep(2)
def script_hooks(name, active):
hook_subdir = path.join(HOOK_DIR, '%s_%s.d' % (name, 'open' if active else 'closed'))
return [ path.join(hook_subdir, f) for f in os.listdir(hook_subdir) ]
if __name__ == '__main__':
syslog.openlog('spacestated')
prev_state = get_state()
syslog.syslog(syslog.LOG_INFO, 'Initial state: %s' % ', '.join([ '%s=%s' % (name, 'open' if op else 'closed') for name, op in prev_state.items() ]))
while True:
state = await_state_change(prev_state)
syslog.syslog(syslog.LOG_INFO, 'State: %s' % ', '.join([ '%s=%s' % (name, 'open' if op else 'closed') for name, op in state.items() ]))
try:
scripts = []
for name, active in state.items():
scripts_past = []
scripts_future = []
for scissor_name, scissor_active in state.items():
if scissor_active and name != scissor_name:
scripts_past.extend(script_hooks(scissor_name, True))
scripts_future.extend(script_hooks(scissor_name, False))
hashes_past = { hash_file(f): f for f in scripts_past }
hashes_future = { hash_file(f): f for f in scripts_future }
if active != prev_state[name]:
hashes = { hash_file(f): f for f in script_hooks(name, active) }
# If the state transitions to open, the scripts that were
# ran in the past should not run.
# If the state transitions to closed, the scripts that will
# run in the future should not run.
scissor = hashes_past if active else hashes_future
scripts.append((hashes, scissor))
run = []
for scriptset in scripts:
(hashes, scissor) = scriptset
run.extend([ hashes[h] for h in set(hashes.keys()) - set(scissor.keys()) ])
run.sort()
for script in run:
try:
if os.access(script, os.X_OK):
out = subprocess.check_output([ script ])[:-1].decode('utf8')
syslog.syslog(syslog.LOG_INFO, '-> %s: %s' % (script, out))
except Exception as err:
syslog.syslog(syslog.LOG_ERR, str(err))
except Exception as err:
syslog.syslog(syslog.LOG_ERR, str(err))
time.sleep(1)
prev_state = state