#!/usr/bin/env python3 """ A hub for controlling IOT devices. """ import os import re import copy import json import requests from flask import Flask, render_template, request, abort, jsonify from flask import Blueprint import config import auth from auth import auth_required class RelayDevice: """ Represents a relay receptacle device controlled by an ESP-01. The ESP-01 acts a simple switch to turn each receptable on or off via a relay. Each device only has two outputs, `OUT0` and `OUT2`. """ def __init__(self): self.id = "" self.type = "RelayDevice" self.description = "" self.location = "" self.ip_address = "" self.sub_devices = [] self.sub_devices.append(RelayOutlet()) self.sub_devices.append(RelayOutlet()) self.sub_devices[0].id = 'OUT0' self.sub_devices[0].gpio = '0' self.sub_devices[1].id = 'OUT2' self.sub_devices[1].gpio = '2' self.update() def update(self): """ Queries the physical device and updates the internal model as appropriate. """ if not self.ip_address: return for sub_dev in self.sub_devices: res = requests.get(self.ip_address) gpio0 = re.search(r"GPIO0: (\bLow|\bHigh)", res.text).groups()[0] sub_dev.state = gpio0 == 'High' def toggle(self, sub_dev_id): """ Toggles the state of a sub device. """ for sub_dev in self.sub_devices: if sub_dev.id == sub_dev_id: break else: return None params = {sub_dev.gpio: 'toggle'} res = requests.get('http://' + self.ip_address + '/gpio',params=params) res.raise_for_status() state = re.search( rf"GPIO{sub_dev.gpio}: (\bLow|\bHigh)", res.text ).groups()[0] == 'High' sub_dev.state = state return json.dumps({'ok': True, sub_dev_id: state}) def from_dict(self, data): """ Initializes self with data from JSON dict. """ for key, value in data.items(): setattr(self, key, value) self.sub_devices = [] for sub_dev_dict in data.get('sub_devices'): if sub_dev_dict.get('type') == 'RelayOutlet': sub_dev = RelayOutlet().from_dict(sub_dev_dict) self.sub_devices.append(sub_dev) else: typ = sub_dev.get('type') raise ValueError(f"Unknown sub_device type {typ}") return self class RelayOutlet: """ Represents a single outlet on a RelayDevice. """ def __init__(self): self.id = "" self.type = "RelayOutlet" self.description = "" self.gpio = "" self.state = False def from_dict(self, data): """ Initializes self with data from JSON dict. """ for key, value in data.items(): setattr(self, key, value) return self class DeviceEncoder(json.JSONEncoder): """ A custom json encoder for dumping devices to file. """ def default(self, o): return vars(o) def init_network(filepath="devices.json"): """ Initializes the network of IOT devices. """ with open(filepath, 'r') as file: data = file.read() data = json.loads(data) network = [] for device_dict in data: if device_dict.get('type') == 'RelayDevice': device = RelayDevice().from_dict(device_dict) network.append(device) else: raise ValueError(f"Unknown device type {device.get('type')}") return network def save_network(filepath="devices.json"): """ Dumps the network to file. """ with open(filepath, 'w') as file: file.write(json.dumps(network, cls=DeviceEncoder, indent='\t')) app_views = Blueprint('app_views', __name__) @app_views.route('/') @auth_required def index(): """ The index page. """ init_state = {} for device in network: device_state = {} for sub_dev in device.sub_devices: device_state[sub_dev.id] = sub_dev.state init_state[device.id] = device_state return render_template('index.html', network=network,init_state=init_state) @app_views.route('/toggle') @auth_required def toggle(): """ Toggles the state of a RelayDevice. """ device_id = request.args.get('device_id') sub_dev_id = request.args.get('sub_dev_id') for device in network: if device.id == device_id: break else: return make_error(404, "device_id not found") res = device.toggle(sub_dev_id) if not res: return make_error(404, "sub_dev_id not found") save_network() return res @app_views.route('/edit') @auth_required def edit(): """ Edits the text of a particular field. """ device_id = request.args.get('device_id') sub_dev_id = request.args.get('sub_dev_id') field = request.args.get('field') value = request.args.get('value') for device in network: if device.id == device_id: break else: return make_error(404, "device_id not found") if sub_dev_id: for sub_dev in device.sub_devices: if sub_dev.id == sub_dev_id: break else: return make_error(404, "sub_dev_id not found") if hasattr(sub_dev, field): setattr(sub_dev, field, value) else: return make_error(404, "sub_device field not found") else: if hasattr(device, field): setattr(device, field, value) else: return make_error(404, "device field not found") data = { 'ok': True, 'field': field, 'value': value } save_network() return json.dumps(data) @app_views.route('/new_device') def new_device(): """ Allows adding a new device. Accepts device_type parameter, returns the device_id. """ device_type = request.args.get('device_type') if device_type == 'RelayDevice': device = RelayDevice() else: return make_error(400, "Unknown device type") devices = [dev for dev in network if dev.type == device_type] devices.sort(key=lambda dev: dev.type) if not devices: device.id = device_type + '01' else: num = re.search(r'(\d*)$', devices[-1].id).groups() if not num: device.id = device_type + '01' else: num = str(int(num[0]) + 1).zfill(2) device.id = device_type + num network.append(device) save_network() data = {'device_id': device.id} return json.dumps(data) def make_error(code, message): """ Returns a JSON error. """ res = jsonify(ok=False, status=code, message=message) res.status_code = code return res app = Flask(__name__) app.register_blueprint(app_views, url_prefix=config.url_prefix) app.register_blueprint(auth.auth_views, url_prefix=config.url_prefix) if os.path.isfile('secret_key'): with open('secret_key', 'rb') as file: app.secret_key = file.read() else: secret_key = os.urandom(32) app.secret_key = secret_key with open('secret_key', 'wb') as file: file.write(secret_key) network = init_network() if __name__ == '__main__': app.run(host='0.0.0.0', port=5300)