From 4dddf30c4c0aebe13082b4fb5cf3e636f723b834 Mon Sep 17 00:00:00 2001 From: Morgana Date: Tue, 14 Apr 2026 15:27:11 -0500 Subject: [PATCH] Implemented Lighting Control --- app.py | 124 ++++++++++++++++++++++++++++++++++++++-- static/lighting.css | 78 +++++++++++++++++++++++++ static/lighting.js | 53 +++++++++++++++++ templates/base.html | 2 +- templates/lighting.html | 50 ++++++++++++++++ 5 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 static/lighting.css create mode 100644 static/lighting.js create mode 100644 templates/lighting.html diff --git a/app.py b/app.py index bc2bb31..ecfdf61 100644 --- a/app.py +++ b/app.py @@ -2,19 +2,23 @@ import json from hashlib import scrypt from os import urandom 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_pymongo import PyMongo from bson import ObjectId import bson.json_util as bson +import requests SCRYPT_PARAMS = {"n": 16384, "r": 8, "p": 1} +HEX_CHARS = set("0123456789ABCDEF") app = Flask(__name__) with open("./config.json", "r", encoding="utf-8") as file: config = json.load(file) app.config["MONGO_URI"] = f"mongodb://{config['db']}/akasha" app.config["SECRET_KEY"] = config["fsk"] + LIGHT_URL = f"http://192.168.1.10/api/{config['light']}" mongo = PyMongo( app, username="index", @@ -45,7 +49,7 @@ def inject_data(): @app.before_request def apply_checks(): username = session.get("username") - if request.endpoint is "static": + if request.endpoint == "static": return ddata = db.domains.find_one_or_404({"id": request.endpoint}) 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}) dsdata = db.domains.find({"cat": {"$ne": None}}) 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": {}} for ddata in dsdata: @@ -178,13 +184,10 @@ def database_edit(collection, oid): try: if not document: db[collection].delete_one({"_id": ObjectId(oid)}) - print("DELETED") elif oid: db[collection].replace_one({"_id": ObjectId(oid)}, document) - print("REPLACED") else: db[collection].insert_one(document) - print("INSERTED") except: abort(500) return redirect(url_for("database")) @@ -202,7 +205,6 @@ def database_edit(collection, oid): @app.route("/tasks", methods=["GET", "POST"]) def tasks(): - print(request.form, request.method) if request.method == "POST": act = request.form.get("action", "save") match act: @@ -252,3 +254,113 @@ def tasks(): blocked_tasks=blocked_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) diff --git a/static/lighting.css b/static/lighting.css new file mode 100644 index 0000000..82f8b44 --- /dev/null +++ b/static/lighting.css @@ -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%; + } +} \ No newline at end of file diff --git a/static/lighting.js b/static/lighting.js new file mode 100644 index 0000000..fb0f9db --- /dev/null +++ b/static/lighting.js @@ -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) + } +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index de61fde..9f0e813 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,7 +11,7 @@
- + diff --git a/templates/lighting.html b/templates/lighting.html new file mode 100644 index 0000000..6364e0e --- /dev/null +++ b/templates/lighting.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} + +{% block head %} + + + +{% endblock %} + +{% block content %} +
+ {% for group in ldata %} + {% if ldata[group]["lights"] %} +
+

{{ ldata[group]["name"] }}

+
+ {% for light in ldata[group]["lights"] %} +
+ {% set ldata = ldata[group]["lights"][light] %} +

{{ ldata["name"] }}

+
+
+ + + + +
+ +
+ {% if "ct" in ldata["state"] %} +
+ {{ ldata["state"]["colormode"].upper() }} + + {% if "hue" in ldata["state"] %} + + {% endif %} + +
+ {% endif %} + + +
+ {% endfor %} +
+
+ {% endif %} + {% endfor %} + +{% endblock %} \ No newline at end of file