Implemented Lighting Control
This commit is contained in:
124
app.py
124
app.py
@@ -2,19 +2,23 @@ import json
|
|||||||
from hashlib import scrypt
|
from hashlib import scrypt
|
||||||
from os import urandom
|
from os import urandom
|
||||||
from hmac import compare_digest
|
from hmac import compare_digest
|
||||||
|
from colorsys import rgb_to_hsv
|
||||||
|
|
||||||
from flask import Flask, render_template, request, redirect, url_for, session, abort
|
from flask import Flask, render_template, request, redirect, url_for, session, abort
|
||||||
from flask_pymongo import PyMongo
|
from flask_pymongo import PyMongo
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
import bson.json_util as bson
|
import bson.json_util as bson
|
||||||
|
import requests
|
||||||
|
|
||||||
SCRYPT_PARAMS = {"n": 16384, "r": 8, "p": 1}
|
SCRYPT_PARAMS = {"n": 16384, "r": 8, "p": 1}
|
||||||
|
HEX_CHARS = set("0123456789ABCDEF")
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
with open("./config.json", "r", encoding="utf-8") as file:
|
with open("./config.json", "r", encoding="utf-8") as file:
|
||||||
config = json.load(file)
|
config = json.load(file)
|
||||||
app.config["MONGO_URI"] = f"mongodb://{config['db']}/akasha"
|
app.config["MONGO_URI"] = f"mongodb://{config['db']}/akasha"
|
||||||
app.config["SECRET_KEY"] = config["fsk"]
|
app.config["SECRET_KEY"] = config["fsk"]
|
||||||
|
LIGHT_URL = f"http://192.168.1.10/api/{config['light']}"
|
||||||
mongo = PyMongo(
|
mongo = PyMongo(
|
||||||
app,
|
app,
|
||||||
username="index",
|
username="index",
|
||||||
@@ -45,7 +49,7 @@ def inject_data():
|
|||||||
@app.before_request
|
@app.before_request
|
||||||
def apply_checks():
|
def apply_checks():
|
||||||
username = session.get("username")
|
username = session.get("username")
|
||||||
if request.endpoint is "static":
|
if request.endpoint == "static":
|
||||||
return
|
return
|
||||||
ddata = db.domains.find_one_or_404({"id": request.endpoint})
|
ddata = db.domains.find_one_or_404({"id": request.endpoint})
|
||||||
udata = db.users.find_one({"id": username}) if username else None
|
udata = db.users.find_one({"id": username}) if username else None
|
||||||
@@ -98,7 +102,9 @@ def render_main():
|
|||||||
fpdata = db.frontpage.find_one({"id": request.endpoint})
|
fpdata = db.frontpage.find_one({"id": request.endpoint})
|
||||||
dsdata = db.domains.find({"cat": {"$ne": None}})
|
dsdata = db.domains.find({"cat": {"$ne": None}})
|
||||||
udata = (
|
udata = (
|
||||||
db.users.find_one({"id": session["username"]}) if session["username"] else None
|
db.users.find_one({"id": session["username"]})
|
||||||
|
if session.get("username")
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
results = {"ade": {}, "bea": {}, "cam": {}, "des": {}}
|
results = {"ade": {}, "bea": {}, "cam": {}, "des": {}}
|
||||||
for ddata in dsdata:
|
for ddata in dsdata:
|
||||||
@@ -178,13 +184,10 @@ def database_edit(collection, oid):
|
|||||||
try:
|
try:
|
||||||
if not document:
|
if not document:
|
||||||
db[collection].delete_one({"_id": ObjectId(oid)})
|
db[collection].delete_one({"_id": ObjectId(oid)})
|
||||||
print("DELETED")
|
|
||||||
elif oid:
|
elif oid:
|
||||||
db[collection].replace_one({"_id": ObjectId(oid)}, document)
|
db[collection].replace_one({"_id": ObjectId(oid)}, document)
|
||||||
print("REPLACED")
|
|
||||||
else:
|
else:
|
||||||
db[collection].insert_one(document)
|
db[collection].insert_one(document)
|
||||||
print("INSERTED")
|
|
||||||
except:
|
except:
|
||||||
abort(500)
|
abort(500)
|
||||||
return redirect(url_for("database"))
|
return redirect(url_for("database"))
|
||||||
@@ -202,7 +205,6 @@ def database_edit(collection, oid):
|
|||||||
|
|
||||||
@app.route("/tasks", methods=["GET", "POST"])
|
@app.route("/tasks", methods=["GET", "POST"])
|
||||||
def tasks():
|
def tasks():
|
||||||
print(request.form, request.method)
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
act = request.form.get("action", "save")
|
act = request.form.get("action", "save")
|
||||||
match act:
|
match act:
|
||||||
@@ -252,3 +254,113 @@ def tasks():
|
|||||||
blocked_tasks=blocked_tasks,
|
blocked_tasks=blocked_tasks,
|
||||||
complete_tasks=complete_tasks,
|
complete_tasks=complete_tasks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/lighting", methods=["GET", "POST"])
|
||||||
|
def lighting():
|
||||||
|
print(request.form, request.method)
|
||||||
|
try:
|
||||||
|
if request.method == "POST":
|
||||||
|
act = request.form.get("action", "none")
|
||||||
|
bri = request.form.get("brightness", "0")
|
||||||
|
light = request.form.get("light", "-1")
|
||||||
|
try:
|
||||||
|
bri = int(bri)
|
||||||
|
except:
|
||||||
|
bri = -1
|
||||||
|
try:
|
||||||
|
light = int(light)
|
||||||
|
except:
|
||||||
|
light = -1
|
||||||
|
if light < 0:
|
||||||
|
abort(404)
|
||||||
|
match act:
|
||||||
|
case "toggle":
|
||||||
|
if bri > 0:
|
||||||
|
requests.put(
|
||||||
|
f"{LIGHT_URL}/lights/{light}/state",
|
||||||
|
json={"on": False},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
requests.put(
|
||||||
|
f"{LIGHT_URL}/lights/{light}/state",
|
||||||
|
json={"on": True},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
case "bright":
|
||||||
|
if bri > 0:
|
||||||
|
requests.put(
|
||||||
|
f"{LIGHT_URL}/lights/{light}/state",
|
||||||
|
json={"on": True, "bri": bri},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
requests.put(
|
||||||
|
f"{LIGHT_URL}/lights/{light}/state",
|
||||||
|
json={"on": False},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
case "ctsel":
|
||||||
|
ct = request.form.get("ct", "366")
|
||||||
|
try:
|
||||||
|
ct = int(ct)
|
||||||
|
except:
|
||||||
|
ct = 366
|
||||||
|
print("CT: ", ct)
|
||||||
|
requests.put(
|
||||||
|
f"{LIGHT_URL}/lights/{light}/state",
|
||||||
|
json={"on": True, "ct": ct},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
case "hssel":
|
||||||
|
color = request.form.get("hs", "#FF00AA")
|
||||||
|
if color[0] == "#":
|
||||||
|
color = color[1:]
|
||||||
|
color = color.upper()
|
||||||
|
if len(color) != 6 or not all(c in HEX_CHARS for c in color):
|
||||||
|
color = "FF00AA"
|
||||||
|
rgb = [int(color[i : i + 2], 16) / 255 for i in range(0, 6, 2)]
|
||||||
|
hsv = rgb_to_hsv(*rgb)
|
||||||
|
requests.put(
|
||||||
|
f"{LIGHT_URL}/lights/{light}/state",
|
||||||
|
json={
|
||||||
|
"on": True,
|
||||||
|
"hue": int(hsv[0] * 65535),
|
||||||
|
"sat": int(hsv[1] * 254),
|
||||||
|
"bri": int(hsv[2] * 254),
|
||||||
|
},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
res = requests.get(LIGHT_URL, timeout=5)
|
||||||
|
try:
|
||||||
|
raw = res.json()
|
||||||
|
except:
|
||||||
|
abort(500)
|
||||||
|
result = {
|
||||||
|
group: {
|
||||||
|
"name": raw["groups"][group]["name"],
|
||||||
|
"lights": {},
|
||||||
|
}
|
||||||
|
for group in raw["groups"]
|
||||||
|
}
|
||||||
|
result[-1] = {"name": "Ungrouped", "lights": {}}
|
||||||
|
for light in raw["lights"]:
|
||||||
|
ldata = raw["lights"][light]
|
||||||
|
if not ldata["state"]["on"]:
|
||||||
|
color = "#000000"
|
||||||
|
elif ldata["state"].get("colormode") == "hs":
|
||||||
|
color = f"hsl({ldata['state']['hue']/65535*360} {ldata['state']['sat']*100/256} {ldata['state']['bri']*50/256})"
|
||||||
|
else:
|
||||||
|
color = f"hsl(35 75 {int(ldata["state"]["bri"] / 3)})"
|
||||||
|
ldata["display"] = color
|
||||||
|
ungrouped = True
|
||||||
|
for group in raw["groups"]:
|
||||||
|
if light in raw["groups"][group]["lights"]:
|
||||||
|
result[group]["lights"][light] = ldata
|
||||||
|
ungrouped = False
|
||||||
|
if ungrouped:
|
||||||
|
result[-1]["lights"][light] = ldata
|
||||||
|
return render_template("lighting.html", ldata=result)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
abort(500)
|
||||||
|
|||||||
78
static/lighting.css
Normal file
78
static/lighting.css
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
main {
|
||||||
|
padding: 2em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
background-color: var(--color-bg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group>div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
background-color: var(--color-bg2);
|
||||||
|
width: 12em;
|
||||||
|
height: 15em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.view-symb {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 8em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range][orient=vertical] {
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
direction: rtl;
|
||||||
|
appearance: slider-vertical;
|
||||||
|
width: 0.5em;
|
||||||
|
height: 11em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-wrap: nowrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colormode {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 1px;
|
||||||
|
gap: 1px;
|
||||||
|
|
||||||
|
>output {
|
||||||
|
flex-basis: 2em;
|
||||||
|
text-align: center;
|
||||||
|
align-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
>input {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
>input[type=color] {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
static/lighting.js
Normal file
53
static/lighting.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
function getFirst(item, className) {
|
||||||
|
return item.getElementsByClassName(className)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (lform of document.getElementsByTagName("form")) {
|
||||||
|
var toggleBtn = getFirst(lform, "symb-btn")
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.onclick = (event) => {
|
||||||
|
event.preventDefault(true);
|
||||||
|
var parent = event.target.closest("form");
|
||||||
|
var actionField = getFirst(parent, "action")
|
||||||
|
actionField.value = "toggle";
|
||||||
|
parent.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var brightBar = getFirst(lform, "brightness")
|
||||||
|
if (brightBar) {
|
||||||
|
brightBar.onchange = (event) => {
|
||||||
|
event.preventDefault(true);
|
||||||
|
var parent = event.target.closest("form");
|
||||||
|
var actionField = getFirst(parent, "action")
|
||||||
|
actionField.value = "bright";
|
||||||
|
parent.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctInput = getFirst(lform, "ctsel")
|
||||||
|
if (ctInput) {
|
||||||
|
ctInput.onkeydown = (event) => {
|
||||||
|
if (event.key == "Enter") {
|
||||||
|
event.preventDefault(true);
|
||||||
|
var parent = event.target.closest("form");
|
||||||
|
var actionField = getFirst(parent, "action")
|
||||||
|
actionField.value = "ctsel";
|
||||||
|
parent.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(ctInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hsInput = getFirst(lform, "hssel")
|
||||||
|
if (hsInput) {
|
||||||
|
hsInput.onchange = (event) => {
|
||||||
|
event.preventDefault(true);
|
||||||
|
var parent = event.target.closest("form");
|
||||||
|
var actionField = getFirst(parent, "action")
|
||||||
|
actionField.value = "hssel";
|
||||||
|
parent.submit();
|
||||||
|
}
|
||||||
|
console.log(hsInput)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<svg version=" 1.1" width="196" height="46" xmlns="http://www.w3.org/2000/svg">
|
<svg version="1.1" width="196" height="46" xmlns="http://www.w3.org/2000/svg">
|
||||||
<polyline points="23,46 173,46 196,23 173,0 23,0 0,23" fill="#FFFFFF" class="icon-bg" />
|
<polyline points="23,46 173,46 196,23 173,0 23,0 0,23" fill="#FFFFFF" class="icon-bg" />
|
||||||
<polyline points="23,44 33,34 23,24 13,34" fill="var(--color-max)" style="--hue: var(--des);" />
|
<polyline points="23,44 33,34 23,24 13,34" fill="var(--color-max)" style="--hue: var(--des);" />
|
||||||
<polyline points="34,33 44,23 34,13 24,23" fill="var(--color-max)" style="--hue: var(--bea);" />
|
<polyline points="34,33 44,23 34,13 24,23" fill="var(--color-max)" style="--hue: var(--bea);" />
|
||||||
|
|||||||
50
templates/lighting.html
Normal file
50
templates/lighting.html
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='lighting.css') }}">
|
||||||
|
<script src="{{ url_for('static', filename='lighting.js') }}" defer></script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
{% for group in ldata %}
|
||||||
|
{% if ldata[group]["lights"] %}
|
||||||
|
<div class="group">
|
||||||
|
<h2>{{ ldata[group]["name"] }}</h2>
|
||||||
|
<div>
|
||||||
|
{% for light in ldata[group]["lights"] %}
|
||||||
|
<form method="post" class="light">
|
||||||
|
{% set ldata = ldata[group]["lights"][light] %}
|
||||||
|
<h3>{{ ldata["name"] }}</h3>
|
||||||
|
<div class="view">
|
||||||
|
<div class="view-symb">
|
||||||
|
<svg version="1.1" width="100" height="100" xmlns="http://www.w3.org/2000/svg" class="symb-btn">
|
||||||
|
<polyline points="50,0 100,50 50,100 0,50" fill="#666666" />
|
||||||
|
<polyline points="50,5 95,50 50,95 5,50" fill="{{ ldata['display'] }}" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="range" orient="vertical" class="brightness" name="brightness" min="0" max="254"
|
||||||
|
step="1" value="{{ ldata['state']['bri'] if ldata['state']['on'] else 0 }}">
|
||||||
|
</div>
|
||||||
|
{% if "ct" in ldata["state"] %}
|
||||||
|
<div class="colormode">
|
||||||
|
<output>{{ ldata["state"]["colormode"].upper() }}</output>
|
||||||
|
<input class="ctsel" type="number" min="100" max="600" step="1" name="ct"
|
||||||
|
value="{{ ldata['state']['ct'] }}">
|
||||||
|
{% if "hue" in ldata["state"] %}
|
||||||
|
<input class="hssel" type="color" name="hs" value="{{ ldata['display'] }}">
|
||||||
|
{% endif %}
|
||||||
|
<input type="hidden" name="mode" value="{{ ldata['state']['colormode'] }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<input type="hidden" class="action" name="action" value="none">
|
||||||
|
<input type="hidden" name="light" value="{{ light }}">
|
||||||
|
</form>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user