begin converting AJAX to Websocket

This commit is contained in:
iou1name 2019-11-15 10:35:56 -05:00
parent 43026fa63a
commit 58f1bd2979
5 changed files with 297 additions and 191 deletions

143
events.py Normal file
View File

@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Websocket events."""
import types
import models
from models import network
async def toggle_outlet(ws, data):
"""Toggles the state of a RelayDevice."""
device_id = data.get('device_id')
sub_device_id = data.get('sub_device_id')
device = network.find(device_id)
if not device:
await ws.send_json({'ok': False, 'message': "device_id not found"})
return
state = device.toggle(sub_device_id)
if state is None:
await ws.send_json({'ok': False, 'message': "sub_device_id not found"})
return
models.save_network(network)
data = {}
data['ok'] = True
data['device_id'] = device_id
data['sub_device_id'] = sub_device_id
data['state'] = state
res = {'event': 'toggle_outlet', 'data': data}
await ws.send_json(res)
async def edit(ws, data):
"""Edits the text of a particular field."""
device_id = data.get('device_id')
sub_device_id = data.get('sub_device_id')
field = data.get('field')
value = data.get('value')
device = network.find(device_id)
if not device:
await ws.send_json({'ok': False, 'message': "device_id not found"})
return
if device.locked:
await ws.send_json({'ok': False, 'message': "device is locked for editing"})
return
if sub_device_id:
sub_device = device.find(sub_device_id)
if not sub_device:
await ws.send_json({'ok': False, 'message': "sub_device_id not found"})
return
if hasattr(sub_device, field):
setattr(sub_device, field, value)
else:
await ws.send_json({'ok': False, 'message': "sub_device field not found"})
return
else:
if hasattr(device, field):
setattr(device, field, value)
else:
await ws.send_json({'ok': False, 'message': "device field not found"})
return
res = {
'ok': True,
'field': field,
'value': value
}
models.save_network(network)
await ws.send_json(res)
async def new_device(ws, data):
"""
Allows adding a new device. Accepts device_type parameter, returns
the device_id.
"""
device_type = data.get('device_type')
if device_type == 'RelayDevice':
device = models.RelayDevice()
else:
await ws.send_json({'ok': False, 'message': "unknown device type"})
return
devices = [dev for dev in network if dev.type == device_type]
devices.sort(key=lambda dev: dev.id)
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)
models.save_network(network)
res = {'ok': True, 'device_id': device.id}
await ws.send_json(res)
async def lock_device(ws, data):
"""Locks or unlocks a device to prevent or allow editing it's fields."""
device_id = data.get('device_id')
locked = data.get('locked') == 'true'
device = network.find(device_id)
if not device:
await ws.send_json({'ok': False, 'message': "device_id not found"})
return
if locked:
device.locked = True
else:
device.locked = False
models.save_network(network)
await ws.send_json({'ok': True, 'device_id': device.id, 'locked': device.locked})
async def delete(ws, data):
"""Deletes a device."""
device_id = data.get('device_id')
device = network.find(device_id)
if not device:
await ws.send_json({'ok': False, 'message': "device_id not found"})
return
network.remove(device)
models.save_network(network)
await ws.send_json({'ok': True})
events = {}
for obj in dir():
if type(locals()[obj]) == types.FunctionType:
events[locals()[obj].__name__] = locals()[obj]

162
juice.py
View File

@ -7,177 +7,73 @@ import re
import copy import copy
import json import json
from aiohttp import web from aiohttp import web, WSMsgType
import jinja2 import jinja2
import aiohttp_jinja2 import aiohttp_jinja2
from aiohttp_jinja2 import render_template from aiohttp_jinja2 import render_template
#import uvloop import uvloop
import requests import requests
import config import config
import events
import models import models
import buckler_aiohttp import buckler_aiohttp
from tools import make_error from tools import make_error
from models import network
#uvloop.install() uvloop.install()
network = models.init_network()
routes = web.RouteTableDef() routes = web.RouteTableDef()
@routes.get('/', name='index') @routes.get('/', name='index')
async def index(request): async def index(request):
"""The index page.""" """The index page."""
global network
init_state = {} init_state = {}
for device in network: for device in network:
device_state = {} device_state = {}
for sub_dev in device.sub_devices: for sub_dev in device.sub_devices:
device_state[sub_dev.id] = sub_dev.state device_state[sub_dev.id] = sub_dev.state
init_state[device.id] = device_state init_state[device.id] = device_state
d = {'network': network, 'init_state': init_state} d = {'network': network, 'init_state': init_state, 'request': request}
return render_template('index.html', request, d) return render_template('index.html', request, d)
@routes.get('/toggle', name='toggle') @routes.get('/ws', name='ws')
async def toggle(request): async def websocket_handler(request):
"""Toggles the state of a RelayDevice.""" """The websocket endpoint."""
global network ws = web.WebSocketResponse(heartbeat=30)
device_id = request.query.get('device_id') ws_ready = ws.can_prepare(request)
sub_dev_id = request.query.get('sub_dev_id') if not ws_ready.ok:
return web.Response(text="Cannot start websocket.")
await ws.prepare(request)
for device in network: async for msg in ws:
if device.id == device_id: if msg.type != WSMsgType.TEXT:
break break
else: try:
return make_error(404, "device_id not found") data = json.loads(msg.data)
res = device.toggle(sub_dev_id) except json.JSONDecodeError:
if not res:
return make_error(404, "sub_dev_id not found")
models.save_network(network)
return web.json_response(res)
@routes.get('/edit', name='edit')
async def edit(request):
"""Edits the text of a particular field."""
global network
device_id = request.query.get('device_id')
sub_dev_id = request.query.get('sub_dev_id')
field = request.query.get('field')
value = request.query.get('value')
for device in network:
if device.id == device_id:
break break
else:
return make_error(404, "device_id not found")
if device.locked: event = data.get('event')
return make_error(400, "device is locked for editing") if not event or event not in events.events.keys():
if sub_dev_id:
for sub_dev in device.sub_devices:
if sub_dev.id == sub_dev_id:
break break
else: await events.events[event](ws, data.get('data'))
return make_error(404, "sub_dev_id not found") await ws.close()
if hasattr(sub_dev, field): return ws
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
}
models.save_network(network)
return web.json_response(data)
@routes.get('/new_device', name='new_device')
async def new_device(request):
"""
Allows adding a new device. Accepts device_type parameter, returns
the device_id.
"""
global network
device_type = request.query.get('device_type')
if device_type == 'RelayDevice':
device = models.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)
models.save_network(network)
data = {'device_id': device.id}
return web.json_response(data)
@routes.get('/lock_device', name='lock_device')
async def lock_device(request):
"""Locks or unlocks a device to prevent or allow editing it's fields."""
global network
device_id = request.query.get('device_id')
locked = request.query.get('locked') == 'true'
for device in network:
if device.id == device_id:
break
else:
return make_error(404, "device_id not found")
if locked:
device.locked = True
else:
device.locked = False
models.save_network(network)
return web.json_response({'device_id': device.id, 'locked': device.locked})
@routes.get('/delete', name='delete')
async def delete(request):
"""Deletes a device."""
global network
device_id = request.query.get('device_id')
for device in network:
if device.id == device_id:
break
else:
return make_error(404, "device_id not found")
network.remove(device)
models.save_network(network)
return web.json_response(True)
async def init_app(): async def init_app():
"""Initializes the application.""" """Initializes the application."""
app = web.Application(middlewares=[buckler_aiohttp.buckler_session]) app = web.Application(middlewares=[buckler_aiohttp.buckler_session])
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates')) aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))
#app.on_startup.append(start_background_tasks)
#app.on_cleanup.append(cleanup_background_tasks)
app.router.add_routes(routes) app.router.add_routes(routes)
app_wrap = web.Application() app_wrap = web.Application()
app_wrap.add_subapp(config.url_prefix, app) app_wrap.add_subapp(config.url_prefix, app)
return app_wrap return app_wrap
if __name__ == "__main__":
app = init_app()
aiohttp.web.run_app(app, host='0.0.0.0', port=5300)

View File

@ -7,7 +7,41 @@ import json
import requests import requests
class RelayDevice: class Network(list):
"""
Represents a network of IOT devices.
"""
def find(self, device_id):
"""
Searches through the network and returns the matching device,
or None.
"""
for device in self:
if device.id == device_id:
break
else:
return None
return device
class Device:
"""
Represents a generic device on the Network.
"""
def find(self, sub_device_id):
"""
Searches through the devices sub_devices and returns the matching
sub_device, or None.
"""
for sub_device in self.sub_devices:
if sub_device.id == sub_device_id:
break
else:
return None
return sub_device
class RelayDevice(Device):
""" """
Represents a relay receptacle device controlled by an ESP-01. The 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 ESP-01 acts a simple switch to turn each receptable on or off via
@ -43,27 +77,25 @@ class RelayDevice:
gpio0 = re.search(r"GPIO0: (\bLow|\bHigh)", res.text).groups()[0] gpio0 = re.search(r"GPIO0: (\bLow|\bHigh)", res.text).groups()[0]
sub_dev.state = gpio0 == 'High' sub_dev.state = gpio0 == 'High'
def toggle(self, sub_dev_id): def toggle(self, sub_device_id):
""" """
Toggles the state of a sub device. Toggles the state of a sub device.
""" """
for sub_dev in self.sub_devices: sub_device = self.find(sub_device_id)
if sub_dev.id == sub_dev_id: if not sub_device:
break
else:
return None return None
params = {sub_dev.gpio: 'toggle'} params = {sub_device.gpio: 'toggle'}
res = requests.get('http://' + self.ip_address + '/gpio',params=params) res = requests.get('http://' + self.ip_address + '/gpio',params=params)
res.raise_for_status() res.raise_for_status()
state = re.search( state = re.search(
rf"GPIO{sub_dev.gpio}: (\bLow|\bHigh)", rf"GPIO{sub_device.gpio}: (\bLow|\bHigh)",
res.text res.text
).groups()[0] == 'High' ).groups()[0] == 'High'
sub_dev.state = state sub_device.state = state
return {'ok': True, sub_dev_id: state} return state
def from_dict(self, data): def from_dict(self, data):
""" """
@ -102,7 +134,7 @@ class RelayOutlet:
return self return self
class LightStrip: class LightStrip(Device):
""" """
Represents an RGB LED light strip controller. Represents an RGB LED light strip controller.
""" """
@ -146,7 +178,7 @@ def init_network(filepath="devices.json"):
with open(filepath, 'r') as file: with open(filepath, 'r') as file:
data = file.read() data = file.read()
data = json.loads(data) data = json.loads(data)
network = [] network = Network()
for device_dict in data: for device_dict in data:
if device_dict.get('type') == 'RelayDevice': if device_dict.get('type') == 'RelayDevice':
device = RelayDevice().from_dict(device_dict) device = RelayDevice().from_dict(device_dict)
@ -165,3 +197,6 @@ def save_network(network, filepath="devices.json"):
""" """
with open(filepath, 'w') as file: with open(filepath, 'w') as file:
file.write(json.dumps(network, cls=DeviceEncoder, indent='\t')) file.write(json.dumps(network, cls=DeviceEncoder, indent='\t'))
network = init_network()

View File

@ -1,4 +1,8 @@
var socket;
function load() { function load() {
socket = init_websocket();
Object.entries(init_state).forEach(([device_id, sub_devs]) => { Object.entries(init_state).forEach(([device_id, sub_devs]) => {
let device = document.querySelector('#' + device_id); let device = document.querySelector('#' + device_id);
Object.entries(sub_devs).forEach(([sub_dev_id, state]) => { Object.entries(sub_devs).forEach(([sub_dev_id, state]) => {
@ -18,41 +22,74 @@ function load() {
}); });
} }
function get_object_from_svg(svg) { /* Websocket setup */
var all_objects = document.getElementsByTagName("object"); function init_websocket() {
for (var i=0; i < all_objects.length; i++) { let socket = new Websocket('wss://' + window.location.hostname + ws_uri);
if (svg === all_objects[i].getSVGDocument().firstElementChild) { socket._send = socket.send;
return all_objects[i]; socket.send = function(event_title, data) {
data = JSON.stringify({event: event_title, data: data});
if (socket.readyState == 0) {
console.log("Socket is still opening!");
} }
socket._send(data);
} }
return null; socket.onmessage = onmessage;
socket.onclose = onclose;
socket.onerror = onerror;
socket.events = {};
socket.events['toggle_outlet'] = toggle_outlet_recv;
return socket;
} }
function toggle_outlet(svg) { function onmessage (e) {
let sub_dev = get_object_from_svg(svg).parentElement; let data = JSON.parse(e.data);
let params = { let event = data.event;
device_id: sub_dev.parentElement.parentElement.id, data = data.data;
sub_dev_id: sub_dev.querySelector('.id').innerText, if (socket.events[event] === undefined) { return; }
}; socket.events[event](data);
let query = Object.keys(params) }
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&'); function onclose(e) {
fetch(window.location.href + 'toggle?' + query) if (e.wasClean) { return; } // no need to reconnect
.then(function(response) { console.log(e);
return response.json(); console.log('Websocket lost connection to server. Re-trying...');
}) socket = init_websocket();
.then(function(json) { }
if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); }
if (json[sub_dev.querySelector('.id').innerText]) { function onerror(e) {
console.log("Websocket error!")
console.log(e);
}
/* Websocket receive */
function toggle_outlet_recv(data) {
if (!data.ok) {
throw new Error('Socket Event Error: toggle_outlet, message = ' + data.message);
return;
}
let device = document.querySelector('#' + data.device_id);
let sub_device = document.querySelector('.' + data.sub_device_id);
let svg = sub_device.querySelector('.outlet_image').getSVGDocument().firstChild;
if (data.state === true) {
svg.classList.remove('off'); svg.classList.remove('off');
svg.classList.add('on'); svg.classList.add('on');
} else { } else {
svg.classList.remove('on'); svg.classList.remove('on');
svg.classList.add('off'); svg.classList.add('off');
} }
});
} }
/* Websocket send */
function toggle_outlet(svg) {
let sub_dev = get_object_from_svg(svg).parentElement;
let data = {
device_id: sub_dev.parentElement.parentElement.id,
sub_device_id: sub_dev.querySelector('.id').innerText,
};
socket.send('toggle_outlet', data);
}
/* AJAX */
function edit_field(field) { function edit_field(field) {
let value = field.firstElementChild.innerText; let value = field.firstElementChild.innerText;
let input = document.createElement('input'); let input = document.createElement('input');
@ -217,25 +254,6 @@ function delete_device(device) {
}); });
} }
function delete_key(key_id) {
if (!window.confirm("Are you sure you want to delete this key? This action may leave you unable to access your account.")) { return; }
let params = {
key_id: key_id,
};
let query = Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
fetch(url_prefix + '/delete_key?' + query)
.then(function(response) {
return response.json();
})
.then(function(json) {
if (!json.ok) { return; }
document.querySelector('#key_' + key_id).remove();
});
}
function delete_token(token_id) { function delete_token(token_id) {
if (!window.confirm("Are you sure you want to delete this token?")) { return; } if (!window.confirm("Are you sure you want to delete this token?")) { return; }
let params = { let params = {
@ -254,3 +272,14 @@ function delete_token(token_id) {
document.querySelector('#token_' + token_id).remove(); document.querySelector('#token_' + token_id).remove();
}); });
} }
/* Misc */
function get_object_from_svg(svg) {
let all_objects = document.getElementsByTagName("object");
for (let i=0; i < all_objects.length; i++) {
if (svg === all_objects[i].getSVGDocument().firstElementChild) {
return all_objects[i];
}
}
return null;
}

View File

@ -3,8 +3,11 @@
<head> <head>
<title>Juice</title> <title>Juice</title>
<link rel="stylesheet" type="text/css" href="/static/juice.css"> <link rel="stylesheet" type="text/css" href="/static/juice.css">
<script>
const init_state = {{ init_state|tojson|safe }};
const ws_uri = "{{ request.app.router['ws'].url_for() }}";
</script>
<script type="text/javascript" src="/static/juice.js"></script> <script type="text/javascript" src="/static/juice.js"></script>
<script>var init_state = {{ init_state|tojson|safe }};</script>
<script>window.onload = load;</script> <script>window.onload = load;</script>
<meta name="viewport" content="width=device-width, initial-scale=0.8"> <meta name="viewport" content="width=device-width, initial-scale=0.8">
<meta name="description" content="An IOT hub."> <meta name="description" content="An IOT hub.">
@ -43,7 +46,7 @@
</div> </div>
{% endfor %} {% endfor %}
{% elif device.type == 'LightStrip' %} {% elif device.type == 'LightStrip' %}
{% for n in range(48) %} {% for n in range(24) %}
<div class="sub_device led led{{n}}"> <div class="sub_device led led{{n}}">
<label class="led_color"> <label class="led_color">
<input class="led_color_input" type="color"> <input class="led_color_input" type="color">