spacestated/spacestated

106 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' and alarm_disarmed()
def getstate_bitlair():
return mqtt_get('bitlair/switch/state') == 'bitlair' and alarm_disarmed()
def get_state():
return {
'bitlair': getstate_bitlair(),
'djo': getstate_djo(),
}
def await_state_change(prev_state):
# FIXME: Changing the state using a switch while the space is cycling will be ignored.
# FIXME: If the alarm is closed while the space is being opened, the state will stay open.
# Receive 3 messages from MQTT. The first two messages are retained.
# The last one will be acted upon.
# This is not the cleanest solution...
while True:
cmd = ['mqtt-simple', '--count', '3', '-h', MQTT_HOST, '-s', 'bitlair/switch/state', '-s', 'bitlair/alarm']
subprocess.check_output(cmd)[:-1].decode('utf8')
state = get_state()
if state != prev_state:
return state
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 = []
scripts_inverse = []
# Build a list of scripts that should be run.
for name, active in state.items():
if active != prev_state[name]:
# Add the scripts that should run.
hook_dir = path.join(HOOK_DIR, '%s_%s.d' % (name, 'open' if active else 'closed'))
scripts.extend([ path.join(hook_dir, f) for f in os.listdir(hook_dir) ])
# Add the scripts that should run if the state is the
# opposite. This will help us to determine whether we will
# be running scripts in needless succession.
inv_dir = path.join(HOOK_DIR, '%s_%s.d' % (name, 'open' if not active else 'closed'))
scripts_inverse.extend([ path.join(inv_dir, f) for f in os.listdir(inv_dir) ])
# Alias scripts to their contents though a hash of each of their contents.
hashes = { hash_file(f): f for f in scripts }
hashes_inverse = { hash_file(f): f for f in scripts_inverse }
run = [ hashes[h] for h in set(hashes.keys()) - set(hashes_inverse.keys()) ]
run.sort()
for script in run:
try:
if os.access(script, os.X_OK):
out = subprocess.check_output([ script ])[:-1].decode('utf8')
print(out)
except Exception as err:
print(err)
except Exception as err:
print(err)
time.sleep(1)
prev_state = state