Compare commits
3 Commits
fec387cf6c
...
b9e5745331
Author | SHA1 | Date | |
---|---|---|---|
b9e5745331 | |||
0b17c76fae | |||
952d725639 |
92
juice.py
92
juice.py
|
@ -3,10 +3,11 @@
|
||||||
A hub for controlling IOT devices.
|
A hub for controlling IOT devices.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from flask import Flask, render_template, request, abort
|
from flask import Flask, render_template, request, abort, jsonify
|
||||||
|
|
||||||
|
|
||||||
class RelayDevice:
|
class RelayDevice:
|
||||||
|
@ -54,17 +55,14 @@ class RelayDevice:
|
||||||
).groups()[0] == 'High'
|
).groups()[0] == 'High'
|
||||||
sub_dev.state = state
|
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):
|
def from_dict(self, data):
|
||||||
"""
|
"""
|
||||||
Initializes self with data from JSON dict.
|
Initializes self with data from JSON dict.
|
||||||
"""
|
"""
|
||||||
self.id = data['id']
|
for key, value in data.items():
|
||||||
self.description = data['description']
|
setattr(self, key, value)
|
||||||
self.location = data['location']
|
|
||||||
self.ip_address = data['ip_address']
|
|
||||||
self.sub_devices = {}
|
|
||||||
for sub_dev_type, sub_devs in data['sub_devices'].items():
|
for sub_dev_type, sub_devs in data['sub_devices'].items():
|
||||||
if sub_dev_type == 'RelayOutlet':
|
if sub_dev_type == 'RelayOutlet':
|
||||||
self.sub_devices[sub_dev_type] = []
|
self.sub_devices[sub_dev_type] = []
|
||||||
|
@ -90,18 +88,24 @@ class RelayOutlet:
|
||||||
"""
|
"""
|
||||||
Initializes self with data from JSON dict.
|
Initializes self with data from JSON dict.
|
||||||
"""
|
"""
|
||||||
self.id = data['id']
|
for key, value in data.items():
|
||||||
self.description = data['description']
|
setattr(self, key, value)
|
||||||
self.gpio = data['gpio']
|
|
||||||
self.state = data['state']
|
|
||||||
return self
|
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.
|
Initializes the network of IOT devices.
|
||||||
"""
|
"""
|
||||||
with open("devices.json", '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 = {}
|
||||||
|
@ -116,6 +120,14 @@ def init_network():
|
||||||
return 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__)
|
app = Flask(__name__)
|
||||||
network = init_network()
|
network = init_network()
|
||||||
|
|
||||||
|
@ -139,10 +151,60 @@ def toggle():
|
||||||
if device.id == device_id:
|
if device.id == device_id:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
abort(404)
|
return make_error(404, "device_id not found")
|
||||||
res = device.toggle(sub_dev_id)
|
res = device.toggle(sub_dev_id)
|
||||||
if not res:
|
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
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
static/fontawesome-webfont.woff2
Normal file
BIN
static/fontawesome-webfont.woff2
Normal file
Binary file not shown.
|
@ -2,15 +2,26 @@ body {
|
||||||
padding: 5%;
|
padding: 5%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
font-family: Helvetica,sans-serif;
|
||||||
|
background-color: lightblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device {
|
.device {
|
||||||
border: 1px solid #ccc;
|
border: 2px solid darkgray;
|
||||||
padding: 3%;
|
border-radius: 0.5em;
|
||||||
margin: 3%;
|
padding: 3em;
|
||||||
|
margin: 3em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background-color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ip_address {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: darkgray;
|
||||||
|
margin: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub_devices {
|
.sub_devices {
|
||||||
|
@ -18,7 +29,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub_device {
|
.sub_device {
|
||||||
border: 1px solid #ccc;
|
border: 2px solid darkgray;
|
||||||
|
border-radius: 0.5em;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -42,3 +54,19 @@ path {
|
||||||
.off {
|
.off {
|
||||||
stroke: black;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@ function toggle_outlet(svg) {
|
||||||
.join('&');
|
.join('&');
|
||||||
fetch(window.location.href + 'toggle?' + query)
|
fetch(window.location.href + 'toggle?' + query)
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
if (!response.ok) { throw new Error('HTTP error, status = ' + response.status); }
|
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(function(json) {
|
.then(function(json) {
|
||||||
|
if (!json.ok) { throw new Error('HTTP error, status = ' + json.status + ', message = ' + json.message); }
|
||||||
if (json[sub_dev.id]) {
|
if (json[sub_dev.id]) {
|
||||||
svg.classList.remove('off');
|
svg.classList.remove('off');
|
||||||
svg.classList.add('on');
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -10,9 +10,9 @@
|
||||||
{% for device in devices %}
|
{% for device in devices %}
|
||||||
<div class="device {{ dev_type }}" id="{{ device.id }}">
|
<div class="device {{ dev_type }}" id="{{ device.id }}">
|
||||||
<div class="id">{{ device.id }}</div>
|
<div class="id">{{ device.id }}</div>
|
||||||
<div class="description">{{ device.description }}</div>
|
<div class="description"><span class="field_value">{{ device.description }}</span> <span class="edit" onclick="edit_field(this.parentElement)"></span></div>
|
||||||
<div class="location">{{ device.location }}</div>
|
<div class="location"><span class="field_value">{{ device.location }}</span> <span class="edit" onclick="edit_field(this.parentElement)"></span></div>
|
||||||
<div class="ip_address">{{ device.ip_address }}</div>
|
<div class="ip_address"><i>{{ device.ip_address }}</i></div>
|
||||||
<div class="sub_devices">
|
<div class="sub_devices">
|
||||||
{% for sub_dev_type, sub_devs in device.sub_devices.items() %}
|
{% for sub_dev_type, sub_devs in device.sub_devices.items() %}
|
||||||
{% for sub_dev in sub_devs %}
|
{% 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="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)" />
|
<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>
|
</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)"></span></div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user