Compare commits
No commits in common. "68fc7192394d4b2196a83e0e9d1f6002e31e1d1d" and "3434fb8f96ac85d966a851f6cc068685cac88db9" have entirely different histories.
68fc719239
...
3434fb8f96
|
@ -3,7 +3,7 @@ A hub for controlling IOT devices.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
Python 3.6+
|
Python 3.6+
|
||||||
Python packages: `gunicorn aiohttp aiohttp_jinja2 uvloop requests paho-mqtt`
|
Python packages: `gunicorn aiohttp aiohttp_jinja2 uvloop requests`
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
1. Get on the floor
|
1. Get on the floor
|
||||||
|
|
35
events.py
35
events.py
|
@ -3,7 +3,6 @@
|
||||||
import re
|
import re
|
||||||
import types
|
import types
|
||||||
|
|
||||||
import tools
|
|
||||||
import models
|
import models
|
||||||
from models import network
|
from models import network
|
||||||
|
|
||||||
|
@ -105,13 +104,6 @@ async def new_device(request, ws, data):
|
||||||
|
|
||||||
if device_type == 'RelayDevice':
|
if device_type == 'RelayDevice':
|
||||||
device = models.RelayDevice()
|
device = models.RelayDevice()
|
||||||
device.sub_devices.append(models.RelayOutlet())
|
|
||||||
device.sub_devices.append(models.RelayOutlet())
|
|
||||||
device.sub_devices[0].id = 'OUT0'
|
|
||||||
device.sub_devices[0].gpio = '0'
|
|
||||||
device.sub_devices[1].id = 'OUT2'
|
|
||||||
device.sub_devices[1].gpio = '2'
|
|
||||||
device.update()
|
|
||||||
else:
|
else:
|
||||||
data['ok'] = False
|
data['ok'] = False
|
||||||
data['error'] = "unknown device type"
|
data['error'] = "unknown device type"
|
||||||
|
@ -184,33 +176,6 @@ async def delete_device(request, ws, data):
|
||||||
await request.app.send_json_all(res)
|
await request.app.send_json_all(res)
|
||||||
|
|
||||||
|
|
||||||
async def neopixel(request, ws, data):
|
|
||||||
"""Changes the state of a NeoPixel strip."""
|
|
||||||
device_id = data.get('device_id')
|
|
||||||
|
|
||||||
device = network.find(device_id)
|
|
||||||
if not device:
|
|
||||||
data['ok'] = False
|
|
||||||
data['error'] = "device_id not found"
|
|
||||||
await ws.send_json(data)
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO: form validation
|
|
||||||
|
|
||||||
mqtt_msg = [device.mqtt_root]
|
|
||||||
mqtt_msg.append(data.get('change_mode'))
|
|
||||||
mqtt_msg.append(data.get('type'))
|
|
||||||
if data.get('type') == 'solid':
|
|
||||||
mqtt_msg.append(data.get('amount'))
|
|
||||||
if data.get('amount') == 'single':
|
|
||||||
mqtt_msg.append(data.get('sub_device_id').replace('led', ''))
|
|
||||||
mqtt_msg = '/'.join(mqtt_msg)
|
|
||||||
payload = tools.from_html_color(data.get('color'))
|
|
||||||
payload = str(payload)[1:-1].replace(' ', '')
|
|
||||||
request.app['mqtt'].publish(mqtt_msg, payload)
|
|
||||||
# websocket response is handled under LightStrip.mqtt_callback
|
|
||||||
|
|
||||||
|
|
||||||
events = {}
|
events = {}
|
||||||
for obj in dir():
|
for obj in dir():
|
||||||
if type(locals()[obj]) == types.FunctionType:
|
if type(locals()[obj]) == types.FunctionType:
|
||||||
|
|
24
juice.py
24
juice.py
|
@ -14,12 +14,11 @@ from aiohttp_jinja2 import render_template
|
||||||
import uvloop
|
import uvloop
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import mqtt
|
|
||||||
import tools
|
|
||||||
import config
|
import config
|
||||||
import events
|
import events
|
||||||
import models
|
import models
|
||||||
import buckler_aiohttp
|
import buckler_aiohttp
|
||||||
|
from tools import make_error
|
||||||
from models import network
|
from models import network
|
||||||
|
|
||||||
uvloop.install()
|
uvloop.install()
|
||||||
|
@ -30,8 +29,6 @@ async def index(request):
|
||||||
"""The index page."""
|
"""The index page."""
|
||||||
init_state = {}
|
init_state = {}
|
||||||
for device in network:
|
for device in network:
|
||||||
if device.type != "RelayDevice":
|
|
||||||
continue
|
|
||||||
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
|
||||||
|
@ -73,28 +70,11 @@ async def send_json_all(self, d):
|
||||||
await ws.send_json(d)
|
await ws.send_json(d)
|
||||||
|
|
||||||
|
|
||||||
async def start_background_tasks(app):
|
|
||||||
app['mqtt'] = mqtt.init_client(app)
|
|
||||||
|
|
||||||
|
|
||||||
async def cleanup_background_tasks(app):
|
|
||||||
app['mqtt'].loop_stop()
|
|
||||||
|
|
||||||
|
|
||||||
async def init_app():
|
async def init_app():
|
||||||
"""Initializes the application."""
|
"""Initializes the application."""
|
||||||
web.Application.send_json_all = send_json_all
|
web.Application.send_json_all = send_json_all
|
||||||
app = web.Application(middlewares=[buckler_aiohttp.buckler_session])
|
app = web.Application(middlewares=[buckler_aiohttp.buckler_session])
|
||||||
aiohttp_jinja2.setup(
|
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))
|
||||||
app,
|
|
||||||
trim_blocks=True,
|
|
||||||
lstrip_blocks=True,
|
|
||||||
undefined=jinja2.StrictUndefined,
|
|
||||||
loader=jinja2.FileSystemLoader('templates'),
|
|
||||||
filters={'html_color': tools.to_html_color},
|
|
||||||
)
|
|
||||||
app.on_startup.append(start_background_tasks)
|
|
||||||
app.on_cleanup.append(cleanup_background_tasks)
|
|
||||||
app['websockets'] = []
|
app['websockets'] = []
|
||||||
|
|
||||||
app.router.add_routes(routes)
|
app.router.add_routes(routes)
|
||||||
|
|
149
models.py
149
models.py
|
@ -5,8 +5,6 @@ Data models for different device types managed by the Juice IOT hub.
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import tools
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
class Network(list):
|
class Network(list):
|
||||||
|
@ -30,16 +28,6 @@ class Device:
|
||||||
"""
|
"""
|
||||||
Represents a generic device on the Network.
|
Represents a generic device on the Network.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
|
||||||
self.id = ""
|
|
||||||
self.type = ""
|
|
||||||
self.description = ""
|
|
||||||
self.location = ""
|
|
||||||
self.ip_address = ""
|
|
||||||
self.mqtt_root = ""
|
|
||||||
self.locked = False
|
|
||||||
self.sub_devices = []
|
|
||||||
|
|
||||||
def find(self, sub_device_id):
|
def find(self, sub_device_id):
|
||||||
"""
|
"""
|
||||||
Searches through the devices sub_devices and returns the matching
|
Searches through the devices sub_devices and returns the matching
|
||||||
|
@ -52,47 +40,6 @@ class Device:
|
||||||
return None
|
return None
|
||||||
return sub_device
|
return sub_device
|
||||||
|
|
||||||
def from_dict(self, data):
|
|
||||||
"""
|
|
||||||
Initializes self with data from dict.
|
|
||||||
"""
|
|
||||||
for key, value in data.items():
|
|
||||||
if key == 'sub_devices':
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
setattr(self, key, value)
|
|
||||||
for sub_device_dict in data.get('sub_devices'):
|
|
||||||
if sub_device_dict.get('type') == 'RelayOutlet':
|
|
||||||
sub_device = RelayOutlet().from_dict(sub_device_dict)
|
|
||||||
self.sub_devices.append(sub_device)
|
|
||||||
elif sub_device_dict.get('type') == 'NeoPixel':
|
|
||||||
sub_device = NeoPixel().from_dict(sub_device_dict)
|
|
||||||
self.sub_devices.append(sub_device)
|
|
||||||
else:
|
|
||||||
typ = sub_device_dict.get('type')
|
|
||||||
raise ValueError(f"Unknown sub_device type {typ}")
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def mqtt_callback(self, msg):
|
|
||||||
"""
|
|
||||||
Called when the MQTT client receives a message directed to this
|
|
||||||
device. Should be overridden by the device implementation.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SubDevice:
|
|
||||||
"""
|
|
||||||
Represents a generic subdevice in a Device.
|
|
||||||
"""
|
|
||||||
def from_dict(self, data):
|
|
||||||
"""
|
|
||||||
Initializes self with data from dict.
|
|
||||||
"""
|
|
||||||
for key, value in data.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class RelayDevice(Device):
|
class RelayDevice(Device):
|
||||||
"""
|
"""
|
||||||
|
@ -101,8 +48,22 @@ class RelayDevice(Device):
|
||||||
a relay. Each device only has two outputs, `OUT0` and `OUT2`.
|
a relay. Each device only has two outputs, `OUT0` and `OUT2`.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
self.id = ""
|
||||||
self.type = "RelayDevice"
|
self.type = "RelayDevice"
|
||||||
|
self.description = ""
|
||||||
|
self.location = ""
|
||||||
|
self.ip_address = ""
|
||||||
|
self.locked = False
|
||||||
|
|
||||||
|
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):
|
def update(self):
|
||||||
"""
|
"""
|
||||||
|
@ -136,8 +97,24 @@ class RelayDevice(Device):
|
||||||
|
|
||||||
return state
|
return 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(SubDevice):
|
|
||||||
|
class RelayOutlet:
|
||||||
"""
|
"""
|
||||||
Represents a single outlet on a RelayDevice.
|
Represents a single outlet on a RelayDevice.
|
||||||
"""
|
"""
|
||||||
|
@ -148,46 +125,42 @@ class RelayOutlet(SubDevice):
|
||||||
self.gpio = ""
|
self.gpio = ""
|
||||||
self.state = False
|
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 LightStrip(Device):
|
class LightStrip(Device):
|
||||||
"""
|
"""
|
||||||
Represents an RGB LED light strip controller.
|
Represents an RGB LED light strip controller.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.type = "LightStrip"
|
|
||||||
|
|
||||||
async def mqtt_callback(self, app, msg):
|
|
||||||
topic = msg.topic.split('/')
|
|
||||||
payload = msg.payload.decode('utf8').split(',')
|
|
||||||
payload = [int(num) for num in payload]
|
|
||||||
|
|
||||||
sub_device = self.find('led' + topic[4])
|
|
||||||
sub_device.color = payload
|
|
||||||
save_network(network)
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
data['ok'] = True
|
|
||||||
data['device_id'] = self.id
|
|
||||||
data['change_mode'] = topic[1]
|
|
||||||
data['type'] = topic[2]
|
|
||||||
if topic[2] == 'solid':
|
|
||||||
data['amount'] = topic[3]
|
|
||||||
if topic[3] == 'single':
|
|
||||||
data['sub_device_id'] = 'led' + topic[4]
|
|
||||||
data['color'] = tools.to_html_color(payload)
|
|
||||||
res = {'event': 'neopixel', 'data': data}
|
|
||||||
await app.send_json_all(res)
|
|
||||||
|
|
||||||
|
|
||||||
class NeoPixel(SubDevice):
|
|
||||||
"""
|
|
||||||
Represents a single NeoPixel in a LightStrip.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.id = ""
|
self.id = ""
|
||||||
self.type = "NeoPixel"
|
self.type = "LightStrip"
|
||||||
self.color = [0,0,0] # JSON doesn't have tuples
|
self.description = ""
|
||||||
|
self.location = ""
|
||||||
|
self.ip_address = ""
|
||||||
|
self.locked = False
|
||||||
|
|
||||||
|
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 DeviceEncoder(json.JSONEncoder):
|
class DeviceEncoder(json.JSONEncoder):
|
||||||
|
|
38
mqtt.py
38
mqtt.py
|
@ -1,38 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Contains MQTT funtionality.
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
|
||||||
|
|
||||||
from models import network
|
|
||||||
|
|
||||||
def on_connect(client, userdata, flags, rc):
|
|
||||||
"""Called when the client successfully connects to the broker."""
|
|
||||||
client.subscribe("juice/#")
|
|
||||||
for device in network:
|
|
||||||
if device.mqtt_root:
|
|
||||||
client.subscribe(device.mqtt_root + '/#')
|
|
||||||
|
|
||||||
|
|
||||||
def on_message(client, userdata, msg):
|
|
||||||
"""Called when the client receives a message."""
|
|
||||||
root = msg.topic.partition('/')[0]
|
|
||||||
devices = [device for device in network if root == device.mqtt_root]
|
|
||||||
for device in devices:
|
|
||||||
coro = device.mqtt_callback(client.app, msg)
|
|
||||||
future = asyncio.run_coroutine_threadsafe(coro, client.app.loop)
|
|
||||||
future.result()
|
|
||||||
|
|
||||||
|
|
||||||
def init_client(app):
|
|
||||||
"""Initializes the MQTT client."""
|
|
||||||
client = mqtt.Client()
|
|
||||||
client.app = app
|
|
||||||
client.on_connect = on_connect
|
|
||||||
client.on_message = on_message
|
|
||||||
|
|
||||||
client.connect_async("localhost", 1883, 60)
|
|
||||||
client.loop_start()
|
|
||||||
return client
|
|
|
@ -79,7 +79,7 @@ nav span:hover {
|
||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip_address, .mqtt_root {
|
.ip_address {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: dimgray;
|
color: dimgray;
|
||||||
|
@ -109,18 +109,18 @@ nav span:hover {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.NeoPixel {
|
.led {
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
margin: 0.125em;
|
margin: 0.125em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.NeoPixel_color {
|
.led_color {
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.NeoPixel_color_input {
|
.led_color_input {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
display: block;
|
display: block;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
|
|
|
@ -3,7 +3,6 @@ var socket;
|
||||||
function load() {
|
function load() {
|
||||||
socket = init_websocket();
|
socket = init_websocket();
|
||||||
|
|
||||||
// initialize RelayOutlet SVG color
|
|
||||||
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_device_id, state]) => {
|
Object.entries(sub_devs).forEach(([sub_device_id, state]) => {
|
||||||
|
@ -16,10 +15,11 @@ function load() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/*document.querySelectorAll('input[type=color]').forEach((led_input) => {
|
document.querySelectorAll('input[type=color]').forEach((led_input) => {
|
||||||
led_input.onchange = function(event) {
|
led_input.onchange = function(event) {
|
||||||
|
event.target.parentElement.style.backgroundColor = event.target.value;
|
||||||
}
|
}
|
||||||
});*/
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Websocket setup */
|
/* Websocket setup */
|
||||||
|
@ -43,7 +43,6 @@ function init_websocket() {
|
||||||
socket.events['new_device'] = new_device_recv;
|
socket.events['new_device'] = new_device_recv;
|
||||||
socket.events['lock_device'] = lock_device_recv;
|
socket.events['lock_device'] = lock_device_recv;
|
||||||
socket.events['delete_device'] = delete_device_recv;
|
socket.events['delete_device'] = delete_device_recv;
|
||||||
socket.events['neopixel'] = neopixel_recv;
|
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +51,7 @@ function onmessage (e) {
|
||||||
let event = data.event;
|
let event = data.event;
|
||||||
data = data.data;
|
data = data.data;
|
||||||
if (!data.ok) {
|
if (!data.ok) {
|
||||||
throw new Error("Socket error: event = " + event + ", error = " + data.error);
|
throw new Error("Socket error: event = " + event +", error = " + data.error);
|
||||||
}
|
}
|
||||||
if (socket.events[event] === undefined) {
|
if (socket.events[event] === undefined) {
|
||||||
console.log("Unknown socket event: " + event);
|
console.log("Unknown socket event: " + event);
|
||||||
|
@ -61,12 +60,11 @@ function onmessage (e) {
|
||||||
socket.events[event](data);
|
socket.events[event](data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onclose(e) {
|
function onclose(e) {
|
||||||
if (e.wasClean) { return; } // no need to reconnect
|
if (e.wasClean) { return; } // no need to reconnect
|
||||||
console.log(e);
|
console.log(e);
|
||||||
console.log('Websocket lost connection to server. Re-trying...');
|
console.log('Websocket lost connection to server. Re-trying...');
|
||||||
socket = init_websocket();
|
socket = init_websocket();
|
||||||
await sleep(5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onerror(e) {
|
function onerror(e) {
|
||||||
|
@ -164,12 +162,6 @@ function delete_device_recv(data) {
|
||||||
device.remove()
|
device.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
function neopixel_recv(data) {
|
|
||||||
let device = document.querySelector('#' + data.device_id);
|
|
||||||
let sub_device = device.querySelector('.' + data.sub_device_id);
|
|
||||||
sub_device.firstElementChild.style.backgroundColor = data.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Websocket send */
|
/* Websocket send */
|
||||||
function toggle_outlet(svg) {
|
function toggle_outlet(svg) {
|
||||||
let sub_dev = get_object_from_svg(svg).parentElement;
|
let sub_dev = get_object_from_svg(svg).parentElement;
|
||||||
|
@ -231,18 +223,6 @@ function delete_device(device) {
|
||||||
socket.send('delete_device', data);
|
socket.send('delete_device', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function neopixel(device) {
|
|
||||||
let data = {
|
|
||||||
device_id: device.parentElement.parentElement.id,
|
|
||||||
sub_device_id: device.classList[2],
|
|
||||||
change_mode: 'state',
|
|
||||||
type: 'solid',
|
|
||||||
amount: 'single',
|
|
||||||
color: device.firstElementChild.firstElementChild.value
|
|
||||||
}
|
|
||||||
socket.send('neopixel', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* DOM */
|
/* DOM */
|
||||||
function edit_field(field) {
|
function edit_field(field) {
|
||||||
let value = field.firstElementChild.innerText;
|
let value = field.firstElementChild.innerText;
|
||||||
|
@ -267,7 +247,3 @@ function get_object_from_svg(svg) {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<link rel="stylesheet" type="text/css" href="/static/juice.css">
|
<link rel="stylesheet" type="text/css" href="/static/juice.css">
|
||||||
<script>
|
<script>
|
||||||
const init_state = {{ init_state|tojson|safe }};
|
const init_state = {{ init_state|tojson|safe }};
|
||||||
const ws_uri = "{{ url('ws') }}";
|
const ws_uri = "{{ request.app.router['ws'].url_for() }}";
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" src="/static/juice.js"></script>
|
<script type="text/javascript" src="/static/juice.js"></script>
|
||||||
<script>window.onload = load;</script>
|
<script>window.onload = load;</script>
|
||||||
|
@ -35,25 +35,25 @@
|
||||||
<div class="id editable"><span class="field_value">{{ device.id }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span><span class="delete font-awesome" onclick="delete_device(this.parentElement.parentElement)"></span><span class="lock font-awesome" onclick="lock_device(this.parentElement.parentElement)"></span>{% else %}<span class="unlock font-awesome" onclick="unlock_device(this.parentElement.parentElement)"></span>{% endif %}</div>
|
<div class="id editable"><span class="field_value">{{ device.id }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span><span class="delete font-awesome" onclick="delete_device(this.parentElement.parentElement)"></span><span class="lock font-awesome" onclick="lock_device(this.parentElement.parentElement)"></span>{% else %}<span class="unlock font-awesome" onclick="unlock_device(this.parentElement.parentElement)"></span>{% endif %}</div>
|
||||||
<div class="description editable"><span class="field_value">{{ device.description }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
<div class="description editable"><span class="field_value">{{ device.description }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
||||||
<div class="location editable"><span class="field_value">{{ device.location }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
<div class="location editable"><span class="field_value">{{ device.location }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
||||||
{% if device.type == 'RelayDevice' %}
|
|
||||||
<div class="ip_address editable"><span class="field_value">{{ device.ip_address }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
<div class="ip_address editable"><span class="field_value">{{ device.ip_address }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
||||||
{% elif device.type == 'LightStrip' %}
|
|
||||||
<div class="mqtt_root editable"><span class="field_value">{{ device.ip_address }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="sub_devices">
|
<div class="sub_devices">
|
||||||
{% for sub_device in device.sub_devices %}
|
{% if device.type == 'RelayDevice' %}
|
||||||
|
{% for sub_device in device.sub_devices %}
|
||||||
<div class="sub_device {{ sub_device.type }} {{ sub_device.id }}">
|
<div class="sub_device {{ sub_device.type }} {{ sub_device.id }}">
|
||||||
{% if device.type == 'RelayDevice' %}
|
|
||||||
<div class="id">{{ sub_device.id }}</div>
|
<div class="id">{{ sub_device.id }}</div>
|
||||||
<object class="outlet_image" aria-label="Outlet Image" data="/static/outlet.svg"></object>
|
<object class="outlet_image" aria-label="Outlet Image" data="/static/outlet.svg"></object>
|
||||||
<div class="description editable"><span class="field_value">{{ sub_device.description }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
<div class="description editable"><span class="field_value">{{ sub_device.description }}</span>{% if not device.locked %}<span class="edit font-awesome" onclick="edit_field(this.parentElement)"></span>{% endif %}</div>
|
||||||
{% elif device.type == 'LightStrip' %}
|
</div>
|
||||||
<label class="NeoPixel_color" style="background-color: {{ sub_device.color|html_color }}">
|
{% endfor %}
|
||||||
<input class="NeoPixel_color_input" type="color" value="{{ sub_device.color|html_color }}" onchange="neopixel(this.parentElement.parentElement)">
|
{% elif device.type == 'LightStrip' %}
|
||||||
|
{% for n in range(24) %}
|
||||||
|
<div class="sub_device led led{{n}}">
|
||||||
|
<label class="led_color">
|
||||||
|
<input class="led_color_input" type="color">
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
23
tools.py
23
tools.py
|
@ -2,19 +2,14 @@
|
||||||
"""
|
"""
|
||||||
Miscellaneous tools and helper functions.
|
Miscellaneous tools and helper functions.
|
||||||
"""
|
"""
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
def to_html_color(color):
|
|
||||||
"""Converts a decimal color code to HTML format."""
|
|
||||||
html_color = "#"
|
|
||||||
for i in range(3):
|
|
||||||
html_color += hex(color[i])[2:].zfill(2)
|
|
||||||
return html_color
|
|
||||||
|
|
||||||
def from_html_color(html_color):
|
def make_error(code, message):
|
||||||
"""Converts an HTML color code to decimal list format."""
|
"""
|
||||||
html_color = html_color[1:]
|
Returns a JSON error.
|
||||||
color = []
|
"""
|
||||||
color.append(int(html_color[0:2], base=16))
|
d = {'ok': False, 'status': code, 'message': message}
|
||||||
color.append(int(html_color[2:4], base=16))
|
res = web.json_response(d)
|
||||||
color.append(int(html_color[4:6], base=16))
|
res.set_status(code)
|
||||||
return color
|
return res
|
||||||
|
|
Loading…
Reference in New Issue
Block a user