begin converting AJAX to Websocket
This commit is contained in:
parent
43026fa63a
commit
58f1bd2979
143
events.py
Normal file
143
events.py
Normal 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
162
juice.py
|
@ -7,177 +7,73 @@ import re
|
|||
import copy
|
||||
import json
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp import web, WSMsgType
|
||||
import jinja2
|
||||
import aiohttp_jinja2
|
||||
from aiohttp_jinja2 import render_template
|
||||
#import uvloop
|
||||
import uvloop
|
||||
import requests
|
||||
|
||||
import config
|
||||
import events
|
||||
import models
|
||||
import buckler_aiohttp
|
||||
from tools import make_error
|
||||
from models import network
|
||||
|
||||
#uvloop.install()
|
||||
network = models.init_network()
|
||||
uvloop.install()
|
||||
routes = web.RouteTableDef()
|
||||
|
||||
@routes.get('/', name='index')
|
||||
async def index(request):
|
||||
"""The index page."""
|
||||
global network
|
||||
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
|
||||
d = {'network': network, 'init_state': init_state}
|
||||
d = {'network': network, 'init_state': init_state, 'request': request}
|
||||
return render_template('index.html', request, d)
|
||||
|
||||
|
||||
@routes.get('/toggle', name='toggle')
|
||||
async def toggle(request):
|
||||
"""Toggles the state of a RelayDevice."""
|
||||
global network
|
||||
device_id = request.query.get('device_id')
|
||||
sub_dev_id = request.query.get('sub_dev_id')
|
||||
@routes.get('/ws', name='ws')
|
||||
async def websocket_handler(request):
|
||||
"""The websocket endpoint."""
|
||||
ws = web.WebSocketResponse(heartbeat=30)
|
||||
ws_ready = ws.can_prepare(request)
|
||||
if not ws_ready.ok:
|
||||
return web.Response(text="Cannot start websocket.")
|
||||
await ws.prepare(request)
|
||||
|
||||
for device in network:
|
||||
if device.id == device_id:
|
||||
async for msg in ws:
|
||||
if msg.type != WSMsgType.TEXT:
|
||||
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")
|
||||
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:
|
||||
try:
|
||||
data = json.loads(msg.data)
|
||||
except json.JSONDecodeError:
|
||||
break
|
||||
else:
|
||||
return make_error(404, "device_id not found")
|
||||
|
||||
if device.locked:
|
||||
return make_error(400, "device is locked for editing")
|
||||
|
||||
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
|
||||
}
|
||||
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:
|
||||
event = data.get('event')
|
||||
if not event or event not in events.events.keys():
|
||||
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)
|
||||
await events.events[event](ws, data.get('data'))
|
||||
await ws.close()
|
||||
return ws
|
||||
|
||||
|
||||
async def init_app():
|
||||
"""Initializes the application."""
|
||||
app = web.Application(middlewares=[buckler_aiohttp.buckler_session])
|
||||
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_wrap = web.Application()
|
||||
app_wrap.add_subapp(config.url_prefix, app)
|
||||
return app_wrap
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = init_app()
|
||||
aiohttp.web.run_app(app, host='0.0.0.0', port=5300)
|
||||
|
|
59
models.py
59
models.py
|
@ -7,7 +7,41 @@ import json
|
|||
|
||||
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
|
||||
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]
|
||||
sub_dev.state = gpio0 == 'High'
|
||||
|
||||
def toggle(self, sub_dev_id):
|
||||
def toggle(self, sub_device_id):
|
||||
"""
|
||||
Toggles the state of a sub device.
|
||||
"""
|
||||
for sub_dev in self.sub_devices:
|
||||
if sub_dev.id == sub_dev_id:
|
||||
break
|
||||
else:
|
||||
sub_device = self.find(sub_device_id)
|
||||
if not sub_device:
|
||||
return None
|
||||
|
||||
params = {sub_dev.gpio: 'toggle'}
|
||||
params = {sub_device.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)",
|
||||
rf"GPIO{sub_device.gpio}: (\bLow|\bHigh)",
|
||||
res.text
|
||||
).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):
|
||||
"""
|
||||
|
@ -102,7 +134,7 @@ class RelayOutlet:
|
|||
return self
|
||||
|
||||
|
||||
class LightStrip:
|
||||
class LightStrip(Device):
|
||||
"""
|
||||
Represents an RGB LED light strip controller.
|
||||
"""
|
||||
|
@ -146,7 +178,7 @@ def init_network(filepath="devices.json"):
|
|||
with open(filepath, 'r') as file:
|
||||
data = file.read()
|
||||
data = json.loads(data)
|
||||
network = []
|
||||
network = Network()
|
||||
for device_dict in data:
|
||||
if device_dict.get('type') == 'RelayDevice':
|
||||
device = RelayDevice().from_dict(device_dict)
|
||||
|
@ -165,3 +197,6 @@ def save_network(network, filepath="devices.json"):
|
|||
"""
|
||||
with open(filepath, 'w') as file:
|
||||
file.write(json.dumps(network, cls=DeviceEncoder, indent='\t'))
|
||||
|
||||
|
||||
network = init_network()
|
||||
|
|
117
static/juice.js
117
static/juice.js
|
@ -1,4 +1,8 @@
|
|||
var socket;
|
||||
|
||||
function load() {
|
||||
socket = init_websocket();
|
||||
|
||||
Object.entries(init_state).forEach(([device_id, sub_devs]) => {
|
||||
let device = document.querySelector('#' + device_id);
|
||||
Object.entries(sub_devs).forEach(([sub_dev_id, state]) => {
|
||||
|
@ -18,41 +22,74 @@ function load() {
|
|||
});
|
||||
}
|
||||
|
||||
function get_object_from_svg(svg) {
|
||||
var all_objects = document.getElementsByTagName("object");
|
||||
for (var i=0; i < all_objects.length; i++) {
|
||||
if (svg === all_objects[i].getSVGDocument().firstElementChild) {
|
||||
return all_objects[i];
|
||||
/* Websocket setup */
|
||||
function init_websocket() {
|
||||
let socket = new Websocket('wss://' + window.location.hostname + ws_uri);
|
||||
socket._send = socket.send;
|
||||
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 onmessage (e) {
|
||||
let data = JSON.parse(e.data);
|
||||
let event = data.event;
|
||||
data = data.data;
|
||||
if (socket.events[event] === undefined) { return; }
|
||||
socket.events[event](data);
|
||||
}
|
||||
|
||||
function onclose(e) {
|
||||
if (e.wasClean) { return; } // no need to reconnect
|
||||
console.log(e);
|
||||
console.log('Websocket lost connection to server. Re-trying...');
|
||||
socket = init_websocket();
|
||||
}
|
||||
|
||||
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.add('on');
|
||||
} else {
|
||||
svg.classList.remove('on');
|
||||
svg.classList.add('off');
|
||||
}
|
||||
}
|
||||
|
||||
/* Websocket send */
|
||||
function toggle_outlet(svg) {
|
||||
let sub_dev = get_object_from_svg(svg).parentElement;
|
||||
let params = {
|
||||
let data = {
|
||||
device_id: sub_dev.parentElement.parentElement.id,
|
||||
sub_dev_id: sub_dev.querySelector('.id').innerText,
|
||||
sub_device_id: sub_dev.querySelector('.id').innerText,
|
||||
};
|
||||
let query = Object.keys(params)
|
||||
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&');
|
||||
fetch(window.location.href + 'toggle?' + query)
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(json) {
|
||||
if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); }
|
||||
if (json[sub_dev.querySelector('.id').innerText]) {
|
||||
svg.classList.remove('off');
|
||||
svg.classList.add('on');
|
||||
} else {
|
||||
svg.classList.remove('on');
|
||||
svg.classList.add('off');
|
||||
}
|
||||
});
|
||||
socket.send('toggle_outlet', data);
|
||||
}
|
||||
|
||||
/* AJAX */
|
||||
function edit_field(field) {
|
||||
let value = field.firstElementChild.innerText;
|
||||
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) {
|
||||
if (!window.confirm("Are you sure you want to delete this token?")) { return; }
|
||||
let params = {
|
||||
|
@ -254,3 +272,14 @@ function delete_token(token_id) {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
<head>
|
||||
<title>Juice</title>
|
||||
<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>var init_state = {{ init_state|tojson|safe }};</script>
|
||||
<script>window.onload = load;</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="description" content="An IOT hub.">
|
||||
|
@ -43,7 +46,7 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
{% elif device.type == 'LightStrip' %}
|
||||
{% for n in range(48) %}
|
||||
{% for n in range(24) %}
|
||||
<div class="sub_device led led{{n}}">
|
||||
<label class="led_color">
|
||||
<input class="led_color_input" type="color">
|
||||
|
|
Loading…
Reference in New Issue
Block a user