Compare commits

...

3 Commits

Author SHA1 Message Date
b9e5745331 added edit_field functionality 2019-06-13 10:46:59 -04:00
0b17c76fae network state is saved across app reboots 2019-06-08 03:56:30 -04:00
952d725639 refactor from_dict() 2019-06-08 03:12:03 -04:00
5 changed files with 166 additions and 24 deletions

View File

@ -3,10 +3,11 @@
A hub for controlling IOT devices.
"""
import re
import copy
import json
import requests
from flask import Flask, render_template, request, abort
from flask import Flask, render_template, request, abort, jsonify
class RelayDevice:
@ -54,17 +55,14 @@ class RelayDevice:
).groups()[0] == 'High'
sub_dev.state = state
return json.dumps({sub_dev_id: state})
return json.dumps({'ok': True, sub_dev_id: state})
def from_dict(self, data):
"""
Initializes self with data from JSON dict.
"""
self.id = data['id']
self.description = data['description']
self.location = data['location']
self.ip_address = data['ip_address']
self.sub_devices = {}
for key, value in data.items():
setattr(self, key, value)
for sub_dev_type, sub_devs in data['sub_devices'].items():
if sub_dev_type == 'RelayOutlet':
self.sub_devices[sub_dev_type] = []
@ -90,18 +88,24 @@ class RelayOutlet:
"""
Initializes self with data from JSON dict.
"""
self.id = data['id']
self.description = data['description']
self.gpio = data['gpio']
self.state = data['state']
for key, value in data.items():
setattr(self, key, value)
return self
def init_network():
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("devices.json", 'r') as file:
with open(filepath, 'r') as file:
data = file.read()
data = json.loads(data)
network = {}
@ -116,6 +120,14 @@ def init_network():
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 = Flask(__name__)
network = init_network()
@ -139,10 +151,60 @@ def toggle():
if device.id == device_id:
break
else:
abort(404)
return make_error(404, "device_id not found")
res = device.toggle(sub_dev_id)
if not res:
abort(404)
return make_error(404, "sub_dev_id not found")
save_network()
return res
@app.route('/edit')
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 sum(network.values(), []):
if device.id == device_id:
break
else:
return make_error(404, "device_id not found")
if sub_dev_id:
for sub_dev in sum(device.sub_devices.values(), []):
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)
def make_error(code, message):
"""
Returns a JSON error.
"""
res = jsonify(ok=False, status=code, message=message)
res.status_code = code
return res

Binary file not shown.

View File

@ -2,15 +2,26 @@ body {
padding: 5%;
display: flex;
justify-content: center;
font-family: Helvetica,sans-serif;
background-color: lightblue;
}
.device {
border: 1px solid #ccc;
padding: 3%;
margin: 3%;
border: 2px solid darkgray;
border-radius: 0.5em;
padding: 3em;
margin: 3em;
display: flex;
flex-direction: column;
align-items: center;
background-color: whitesmoke;
}
.ip_address {
font-size: 0.8em;
color: darkgray;
margin: 0.2em;
}
.sub_devices {
@ -18,7 +29,8 @@ body {
}
.sub_device {
border: 1px solid #ccc;
border: 2px solid darkgray;
border-radius: 0.5em;
padding: 1em;
margin: 1em;
display: flex;
@ -42,3 +54,19 @@ path {
.off {
stroke: black;
}
@font-face {
font-family: FontAwesome;
src: url("/static/fontawesome-webfont.woff2");
}
.edit, .save {
font-family: FontAwesome;
font-size: 0.8em;
color: dimgrey;
}
.edit:hover, .save:hover {
color: red;
cursor: pointer;
}

View File

@ -9,10 +9,10 @@ function toggle_outlet(svg) {
.join('&');
fetch(window.location.href + 'toggle?' + query)
.then(function(response) {
if (!response.ok) { throw new Error('HTTP error, status = ' + response.status); }
return response.json();
})
.then(function(json) {
if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); }
if (json[sub_dev.id]) {
svg.classList.remove('off');
svg.classList.add('on');
@ -22,3 +22,55 @@ function toggle_outlet(svg) {
}
});
}
function edit_field(field) {
let value = field.firstElementChild.innerText;
let input = document.createElement('input');
input.value = value;
field.firstElementChild.replaceWith(input);
let save = document.createElement('span');
save.innerHTML = '';
save.className = 'save';
save.setAttribute('onclick', 'save_field(this.parentElement)');
field.children[1].replaceWith(save);
}
function save_field(field) {
let value = field.firstElementChild.value;
let device_id = field.parentElement.id;
let sub_dev_id;
if (field.parentElement.className.includes('sub_device')) {
sub_dev_id = device_id;
device_id = field.parentElement.parentElement.parentElement.id;
} else {
sub_dev_id = '';
}
let params = {
device_id: device_id,
sub_dev_id: sub_dev_id,
field: field.className,
value: value
};
let query = Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
fetch(window.location.href + 'edit?' + query)
.then(function(response) {
return response.json();
})
.then(function(json) {
if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); }
let span = document.createElement('span');
span.innerText = json['value'];
span.className = 'field_value';
field.firstElementChild.replaceWith(span);
let edit = document.createElement('span');
edit.innerHTML = '';
edit.className = 'edit';
edit.setAttribute('onclick', 'edit_field(this.parentElement)');
field.children[1].replaceWith(edit);
});
}

View File

@ -10,9 +10,9 @@
{% for device in devices %}
<div class="device {{ dev_type }}" id="{{ device.id }}">
<div class="id">{{ device.id }}</div>
<div class="description">{{ device.description }}</div>
<div class="location">{{ device.location }}</div>
<div class="ip_address">{{ device.ip_address }}</div>
<div class="description"><span class="field_value">{{ device.description }}</span> <span class="edit" onclick="edit_field(this.parentElement)">&#xe800;</span></div>
<div class="location"><span class="field_value">{{ device.location }}</span> <span class="edit" onclick="edit_field(this.parentElement)">&#xe800;</span></div>
<div class="ip_address"><i>{{ device.ip_address }}</i></div>
<div class="sub_devices">
{% for sub_dev_type, sub_devs in device.sub_devices.items() %}
{% for sub_dev in sub_devs %}
@ -24,7 +24,7 @@
<path fill="none" d="M 43, 75 A 1 1 0 0 1 57,75 L 57,84 L 43,84 Z" />
<path fill="white" fill-opacity="0.0" d="M 23,10 L 77,10 A 50 49 0 0 1 77,91 L 23,91 A 50 49 0 0 1 23,10 Z" onclick="toggle_outlet(this.parentElement)" />
</svg>
<div class="description">{{ sub_dev.description }}</div>
<div class="description"><span class="field_value">{{ sub_dev.description }}</span> <span class="edit" onclick="edit_field(this.parentElement)">&#xe800;</span></div>
</div>
{% endfor %}
{% endfor %}