112 lines
3.7 KiB
Python
Executable file
112 lines
3.7 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import hashlib
|
|
import os
|
|
import os.path as path
|
|
import subprocess
|
|
import time
|
|
|
|
|
|
MQTT_HOST = 'mqtt.bitlair.nl'
|
|
HOOK_DIR = path.dirname(__file__)
|
|
RUN_DIR = '/tmp/spacestate'
|
|
|
|
|
|
def mqtt_get(topic, retry=3):
|
|
try:
|
|
cmd = ['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') == 'open' and alarm_disarmed()
|
|
|
|
def getstate_bitlair():
|
|
return mqtt_get('bitlair/switch/state/bitlair') == 'open' 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:
|
|
print(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__':
|
|
prev_state = get_state()
|
|
while True:
|
|
state = await_state_change(prev_state)
|
|
|
|
print('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')
|
|
print('-> %s: %s' % (script, out))
|
|
except Exception as err:
|
|
print(err)
|
|
|
|
except Exception as err:
|
|
print(err)
|
|
time.sleep(1)
|
|
|
|
prev_state = state
|