lightstrip controls single leds

This commit is contained in:
iou1name 2019-11-18 13:05:37 -05:00
parent da825c1564
commit 68fc719239
9 changed files with 154 additions and 26 deletions

View File

@ -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` Python packages: `gunicorn aiohttp aiohttp_jinja2 uvloop requests paho-mqtt`
## Install ## Install
1. Get on the floor 1. Get on the floor

View File

@ -3,6 +3,7 @@
import re import re
import types import types
import tools
import models import models
from models import network from models import network
@ -183,6 +184,33 @@ 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:

View File

@ -14,11 +14,12 @@ 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()
@ -66,20 +67,20 @@ async def websocket_handler(request):
return ws return ws
def 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
async def send_json_all(self, d): async def send_json_all(self, d):
"""Sends to all connected websockets.""" """Sends to all connected websockets."""
for ws in self['websockets']: for ws in self['websockets']:
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
@ -90,8 +91,10 @@ async def init_app():
lstrip_blocks=True, lstrip_blocks=True,
undefined=jinja2.StrictUndefined, undefined=jinja2.StrictUndefined,
loader=jinja2.FileSystemLoader('templates'), loader=jinja2.FileSystemLoader('templates'),
filters={'html_color': html_color}, 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)

View File

@ -5,6 +5,8 @@ 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):
@ -34,6 +36,7 @@ class Device:
self.description = "" self.description = ""
self.location = "" self.location = ""
self.ip_address = "" self.ip_address = ""
self.mqtt_root = ""
self.locked = False self.locked = False
self.sub_devices = [] self.sub_devices = []
@ -70,6 +73,13 @@ class Device:
raise ValueError(f"Unknown sub_device type {typ}") raise ValueError(f"Unknown sub_device type {typ}")
return self 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: class SubDevice:
""" """
@ -147,6 +157,28 @@ class LightStrip(Device):
super().__init__() super().__init__()
self.type = "LightStrip" 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): class NeoPixel(SubDevice):
""" """

38
mqtt.py Normal file
View File

@ -0,0 +1,38 @@
#!/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

View File

@ -79,7 +79,7 @@ nav span:hover {
background-color: whitesmoke; background-color: whitesmoke;
} }
.ip_address { .ip_address, .mqtt_root {
font-size: 0.8em; font-size: 0.8em;
font-style: italic; font-style: italic;
color: dimgray; color: dimgray;

View File

@ -16,11 +16,10 @@ 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 */
@ -44,6 +43,7 @@ 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 +52,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);
@ -164,6 +164,12 @@ 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;
@ -225,6 +231,18 @@ 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;

View File

@ -35,7 +35,11 @@
<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)">&#xe800;</span><span class="delete font-awesome" onclick="delete_device(this.parentElement.parentElement)">&#xe804;</span><span class="lock font-awesome" onclick="lock_device(this.parentElement.parentElement)">&#xe803;</span>{% else %}<span class="unlock font-awesome" onclick="unlock_device(this.parentElement.parentElement)">&#xf13e;</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)">&#xe800;</span><span class="delete font-awesome" onclick="delete_device(this.parentElement.parentElement)">&#xe804;</span><span class="lock font-awesome" onclick="lock_device(this.parentElement.parentElement)">&#xe803;</span>{% else %}<span class="unlock font-awesome" onclick="unlock_device(this.parentElement.parentElement)">&#xf13e;</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)">&#xe800;</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)">&#xe800;</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)">&#xe800;</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)">&#xe800;</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)">&#xe800;</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)">&#xe800;</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)">&#xe800;</span>{% endif %}</div>
{% endif %}
<div class="sub_devices"> <div class="sub_devices">
{% for sub_device in device.sub_devices %} {% 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 }}">
@ -45,7 +49,7 @@
<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)">&#xe800;</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)">&#xe800;</span>{% endif %}</div>
{% elif device.type == 'LightStrip' %} {% elif device.type == 'LightStrip' %}
<label class="NeoPixel_color" style="background-color: {{ sub_device.color|html_color }}"> <label class="NeoPixel_color" style="background-color: {{ sub_device.color|html_color }}">
<input class="NeoPixel_color_input" type="color" value="{{ sub_device.color|html_color }}"> <input class="NeoPixel_color_input" type="color" value="{{ sub_device.color|html_color }}" onchange="neopixel(this.parentElement.parentElement)">
</label> </label>
{% endif %} {% endif %}
</div> </div>

View File

@ -2,14 +2,19 @@
""" """
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 make_error(code, message): def from_html_color(html_color):
""" """Converts an HTML color code to decimal list format."""
Returns a JSON error. html_color = html_color[1:]
""" color = []
d = {'ok': False, 'status': code, 'message': message} color.append(int(html_color[0:2], base=16))
res = web.json_response(d) color.append(int(html_color[2:4], base=16))
res.set_status(code) color.append(int(html_color[4:6], base=16))
return res return color