feat(zones): persist per-zone brightness and update submodules
Store zone brightness in model/data flow, apply it in the zones UI, and record updated led-driver, led-simulator, and led-tool submodule pointers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
18
.cursor/rules/submodules-led-driver-tool.mdc
Normal file
18
.cursor/rules/submodules-led-driver-tool.mdc
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
description: Keep led-driver and led-tool git submodules in sync when updating led-controller
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Submodule pointers (`led-driver`, `led-tool`)
|
||||||
|
|
||||||
|
This repo tracks **`led-driver`** and **`led-tool`** as git submodules (see `.gitmodules`).
|
||||||
|
|
||||||
|
When you **update led-controller** work that should ship with matching firmware or CLI behaviour—or when you finish changes **inside** those submodule directories—**record the new submodule commits in the parent repo**:
|
||||||
|
|
||||||
|
1. In each submodule, commit and push on its remote if there are local commits (or ensure the checkout is the intended revision).
|
||||||
|
2. From the **led-controller** root: `git add led-driver led-tool` after their HEADs point at the right commits.
|
||||||
|
3. Include the parent-repo commit that bumps the gitlinks (so CI and clones get consistent trees).
|
||||||
|
|
||||||
|
**Do not** leave submodule directories dirty or forgotten while presenting the parent repo as “done”: either commit the submodule pointer update in led-controller, or leave an explicit note if the user must push submodule remotes first.
|
||||||
|
|
||||||
|
If the user only asked for a submodule bump with no code edits, a single `chore(submodules): bump led-driver and led-tool` style commit is appropriate (see commit rule).
|
||||||
@@ -1 +1 @@
|
|||||||
{"188b0e1560a8": {"id": "188b0e1560a8", "name": "led-188b0e1560a8", "type": "led", "transport": "wifi", "address": "10.1.1.192", "default_pattern": null, "zones": []}, "f0f5bdfb9d30": {"id": "f0f5bdfb9d30", "name": "led-f0f5bdfb9d30", "type": "led", "transport": "wifi", "address": "10.1.1.232", "default_pattern": null, "zones": []}}
|
{"188b0e1560a8": {"id": "188b0e1560a8", "name": "led-188b0e1560a8", "type": "led", "transport": "wifi", "address": "10.1.1.192", "default_pattern": null, "zones": []}, "dcb4d99988c8": {"id": "dcb4d99988c8", "name": "outside", "type": "led", "transport": "wifi", "address": "10.1.1.227", "default_pattern": null, "zones": []}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"1": {"name": "default", "names": ["led-188b0e1560a8", "led-f0f5bdfb9d30"], "presets": [["4", "2", "7"], ["3", "14", "5"], ["8", "9", "1"], ["6", "38", "42"], ["39", "40", "41"], ["43", "44", "45"], ["46", "47", "48"], ["49", "50", "51"], ["52", "53", "54"], ["55", "56", "57"], ["58", "59", "60"], ["61", "62"]], "presets_flat": ["4", "2", "7", "3", "14", "5", "8", "9", "1", "6", "38", "42", "39", "40", "41", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62"], "default_preset": "41"}, "2": {"name": "default", "names": ["1", "2", "3", "4", "5", "6", "7", "8", "0", "a"], "presets": [["16", "17", "18"], ["19", "20", "21"], ["22", "23", "24"], ["25", "26", "27"], ["28", "29", "30"]], "presets_flat": ["16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]}, "3": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "4": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "5": {"name": "dj", "names": ["dj"], "presets": [["31", "32", "33"]], "default_preset": "31", "presets_flat": ["31", "32", "33"]}, "6": {"name": "default", "names": ["1"], "presets": [], "default_preset": null, "presets_flat": []}, "7": {"name": "dj", "names": ["dj"], "presets": [["34", "35", "36"]], "default_preset": "34", "presets_flat": ["34", "35", "36"]}, "8": {"name": "test", "names": ["led-188b0e1560a8"], "presets": [["1", "2", "3"], ["4", "5"]], "default_preset": "1", "presets_flat": ["1", "2", "3", "4", "5"]}}
|
{"1": {"name": "default", "names": ["led-188b0e1560a8", "led-f0f5bdfb9d30"], "presets": [["4", "2", "7"], ["3", "14", "5"], ["8", "9", "1"], ["6", "38", "42"], ["39", "40", "41"], ["43", "44", "45"], ["46", "47", "48"], ["49", "50", "51"], ["52", "53", "54"], ["55", "56", "57"], ["58", "59", "60"], ["61", "62"]], "presets_flat": ["4", "2", "7", "3", "14", "5", "8", "9", "1", "6", "38", "42", "39", "40", "41", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62"], "default_preset": "41", "brightness": 23}, "2": {"name": "default", "names": ["1", "2", "3", "4", "5", "6", "7", "8", "0", "a"], "presets": [["16", "17", "18"], ["19", "20", "21"], ["22", "23", "24"], ["25", "26", "27"], ["28", "29", "30"]], "presets_flat": ["16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]}, "3": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "4": {"name": "default", "names": ["1"], "presets": [], "default_preset": null}, "5": {"name": "dj", "names": ["dj"], "presets": [["31", "32", "33"]], "default_preset": "31", "presets_flat": ["31", "32", "33"]}, "6": {"name": "default", "names": ["1"], "presets": [], "default_preset": null, "presets_flat": []}, "7": {"name": "dj", "names": ["dj"], "presets": [["34", "35", "36"]], "default_preset": "34", "presets_flat": ["34", "35", "36"]}, "8": {"name": "test", "names": ["led-188b0e1560a8"], "presets": [["1", "2", "3"], ["4", "5"]], "default_preset": "1", "presets_flat": ["1", "2", "3", "4", "5"], "brightness": 167}}
|
||||||
Submodule led-driver updated: 2fcaf2f064...a79c6f4dd3
Submodule led-simulator updated: 7ce56b64df...42c14361e8
2
led-tool
2
led-tool
Submodule led-tool updated: d6331a105c...580fd11aca
253
scripts/pi-eth-lan-router.sh
Executable file
253
scripts/pi-eth-lan-router.sh
Executable file
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Configure Raspberry Pi OS: Wi-Fi client on IF_WAN (default wlan0), Ethernet IF_LAN
|
||||||
|
# (default eth0) toward an external AP. Static LAN IP, DHCP via dnsmasq, NAT masquerade.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# sudo ./pi-eth-lan-router.sh install
|
||||||
|
# sudo ./pi-eth-lan-router.sh remove
|
||||||
|
#
|
||||||
|
# Environment overrides (optional):
|
||||||
|
# IF_WAN=wlan0 IF_LAN=eth0 LAN_IP=192.168.4.1 LAN_PREFIX=24 \
|
||||||
|
# DHCP_START=192.168.4.100 DHCP_END=192.168.4.200 \
|
||||||
|
# DNSMASQ_DNS=1.1.1.1,8.8.8.8 \
|
||||||
|
# sudo ./pi-eth-lan-router.sh install
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IF_WAN="${IF_WAN:-wlan0}"
|
||||||
|
IF_LAN="${IF_LAN:-eth0}"
|
||||||
|
LAN_IP="${LAN_IP:-192.168.4.1}"
|
||||||
|
LAN_PREFIX="${LAN_PREFIX:-24}"
|
||||||
|
DHCP_START="${DHCP_START:-192.168.4.100}"
|
||||||
|
DHCP_END="${DHCP_END:-192.168.4.200}"
|
||||||
|
# Comma-separated DNS for DHCP clients (Pi does not need to run a resolver).
|
||||||
|
DNSMASQ_DNS="${DNSMASQ_DNS:-1.1.1.1,8.8.8.8}"
|
||||||
|
|
||||||
|
NM_CON_NAME="pi-eth-lan-router"
|
||||||
|
MARK_BEGIN="# BEGIN pi-eth-lan-router (scripts/pi-eth-lan-router.sh)"
|
||||||
|
MARK_END="# END pi-eth-lan-router"
|
||||||
|
SYSCTL_FILE="/etc/sysctl.d/99-pi-eth-lan-router.conf"
|
||||||
|
DNSMASQ_SNIPPET="/etc/dnsmasq.d/pi-eth-lan-router.conf"
|
||||||
|
NFT_SNIPPET="/etc/nftables.d/50-pi-eth-lan-router.nft"
|
||||||
|
NFT_INCLUDE='include "/etc/nftables.d/50-pi-eth-lan-router.nft"'
|
||||||
|
NFTABLES_CONF="/etc/nftables.conf"
|
||||||
|
DHCPCD_CONF="/etc/dhcpcd.conf"
|
||||||
|
|
||||||
|
die() { echo "error: $*" >&2; exit 1; }
|
||||||
|
log() { echo "$*"; }
|
||||||
|
|
||||||
|
need_root() {
|
||||||
|
[[ "${EUID:-0}" -eq 0 ]] || die "run as root (sudo)"
|
||||||
|
}
|
||||||
|
|
||||||
|
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
apt_install() {
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq dnsmasq nftables
|
||||||
|
}
|
||||||
|
|
||||||
|
write_sysctl() {
|
||||||
|
cat >"$SYSCTL_FILE" <<EOF
|
||||||
|
# Managed by scripts/pi-eth-lan-router.sh
|
||||||
|
net.ipv4.ip_forward=1
|
||||||
|
EOF
|
||||||
|
sysctl --system -q 2>/dev/null || sysctl -p "$SYSCTL_FILE" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_sysctl() {
|
||||||
|
rm -f "$SYSCTL_FILE"
|
||||||
|
sysctl --system -q 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
write_dnsmasq() {
|
||||||
|
local mask="255.255.255.0"
|
||||||
|
if [[ "$LAN_PREFIX" != "24" ]]; then
|
||||||
|
die "only LAN_PREFIX=24 is supported by this script (extend dnsmasq netmask manually)"
|
||||||
|
fi
|
||||||
|
cat >"$DNSMASQ_SNIPPET" <<EOF
|
||||||
|
# Managed by scripts/pi-eth-lan-router.sh
|
||||||
|
interface=$IF_LAN
|
||||||
|
bind-interfaces
|
||||||
|
dhcp-range=$DHCP_START,$DHCP_END,$mask,24h
|
||||||
|
dhcp-option=option:router,$LAN_IP
|
||||||
|
dhcp-option=option:dns-server,$DNSMASQ_DNS
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_dnsmasq() {
|
||||||
|
rm -f "$DNSMASQ_SNIPPET"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_nft() {
|
||||||
|
mkdir -p /etc/nftables.d
|
||||||
|
cat >"$NFT_SNIPPET" <<EOF
|
||||||
|
# Managed by scripts/pi-eth-lan-router.sh
|
||||||
|
table ip pi_eth_wlan_nat {
|
||||||
|
chain postrouting {
|
||||||
|
type nat hook postrouting priority 100; policy accept;
|
||||||
|
oifname "$IF_WAN" masquerade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
if [[ -f "$NFTABLES_CONF" ]] && ! grep -qF '50-pi-eth-lan-router.nft' "$NFTABLES_CONF" 2>/dev/null; then
|
||||||
|
printf '\n# pi-eth-lan-router\n%s\n' "$NFT_INCLUDE" >>"$NFTABLES_CONF"
|
||||||
|
elif [[ ! -f "$NFTABLES_CONF" ]]; then
|
||||||
|
log "warning: $NFTABLES_CONF missing; NAT was not added for boot persistence. Install/configure nftables, or add: $NFT_INCLUDE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_nft() {
|
||||||
|
rm -f "$NFT_SNIPPET"
|
||||||
|
if [[ -f "$NFTABLES_CONF" ]]; then
|
||||||
|
sed -i '/# pi-eth-lan-router/d;/50-pi-eth-lan-router\.nft/d' "$NFTABLES_CONF" || true
|
||||||
|
fi
|
||||||
|
nft delete table ip pi_eth_wlan_nat 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_nft() {
|
||||||
|
if have_cmd nft; then
|
||||||
|
nft delete table ip pi_eth_wlan_nat 2>/dev/null || true
|
||||||
|
nft -f "$NFT_SNIPPET"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_nm_eth() {
|
||||||
|
have_cmd nmcli || return 1
|
||||||
|
systemctl is-active --quiet NetworkManager 2>/dev/null || return 1
|
||||||
|
|
||||||
|
if nmcli -t -f NAME con show --active 2>/dev/null | grep -qxF "$NM_CON_NAME"; then
|
||||||
|
nmcli con down "$NM_CON_NAME" || true
|
||||||
|
fi
|
||||||
|
if nmcli -t -f NAME con show 2>/dev/null | grep -qxF "$NM_CON_NAME"; then
|
||||||
|
nmcli con mod "$NM_CON_NAME" \
|
||||||
|
connection.interface-name "$IF_LAN" \
|
||||||
|
ipv4.method manual \
|
||||||
|
ipv4.addresses "${LAN_IP}/${LAN_PREFIX}" \
|
||||||
|
ipv4.gateway "" \
|
||||||
|
ipv4.dns "" \
|
||||||
|
ipv4.never-default yes \
|
||||||
|
ipv6.method ignore
|
||||||
|
else
|
||||||
|
nmcli con add type ethernet con-name "$NM_CON_NAME" ifname "$IF_LAN" \
|
||||||
|
ipv4.method manual \
|
||||||
|
ipv4.addresses "${LAN_IP}/${LAN_PREFIX}" \
|
||||||
|
ipv4.gateway "" \
|
||||||
|
ipv4.dns "" \
|
||||||
|
ipv4.never-default yes \
|
||||||
|
ipv6.method ignore
|
||||||
|
fi
|
||||||
|
if ! nmcli con up "$NM_CON_NAME"; then
|
||||||
|
log "warning: could not activate '$NM_CON_NAME' (is $IF_LAN connected?); profile saved for next boot."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_nm_eth() {
|
||||||
|
have_cmd nmcli || return 0
|
||||||
|
if nmcli -t -f NAME con show 2>/dev/null | grep -qxF "$NM_CON_NAME"; then
|
||||||
|
nmcli con delete "$NM_CON_NAME" || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_dhcpcd_eth() {
|
||||||
|
[[ -f "$DHCPCD_CONF" ]] || return 1
|
||||||
|
if grep -qF "$MARK_BEGIN" "$DHCPCD_CONF" 2>/dev/null; then
|
||||||
|
sed -i "/$MARK_BEGIN/,/$MARK_END/d" "$DHCPCD_CONF" || true
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
echo "$MARK_BEGIN"
|
||||||
|
echo "interface $IF_LAN"
|
||||||
|
echo "static ip_address=${LAN_IP}/${LAN_PREFIX}"
|
||||||
|
echo "nohook wpa_supplicant"
|
||||||
|
echo "$MARK_END"
|
||||||
|
} >>"$DHCPCD_CONF"
|
||||||
|
systemctl restart dhcpcd 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_dhcpcd_block() {
|
||||||
|
[[ -f "$DHCPCD_CONF" ]] || return 0
|
||||||
|
if grep -qF "$MARK_BEGIN" "$DHCPCD_CONF" 2>/dev/null; then
|
||||||
|
sed -i "/$MARK_BEGIN/,/$MARK_END/d" "$DHCPCD_CONF" || true
|
||||||
|
systemctl restart dhcpcd 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_eth_static() {
|
||||||
|
if configure_nm_eth; then
|
||||||
|
log "configured $IF_LAN via NetworkManager profile '$NM_CON_NAME'"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if configure_dhcpcd_eth; then
|
||||||
|
log "configured $IF_LAN via dhcpcd ($DHCPCD_CONF)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
die "neither NetworkManager (active) nor $DHCPCD_CONF found; set $IF_LAN to ${LAN_IP}/${LAN_PREFIX} manually"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_eth_static() {
|
||||||
|
remove_nm_eth
|
||||||
|
remove_dhcpcd_block
|
||||||
|
}
|
||||||
|
|
||||||
|
do_install() {
|
||||||
|
need_root
|
||||||
|
log "installing packages (dnsmasq, nftables)…"
|
||||||
|
apt_install
|
||||||
|
|
||||||
|
log "writing sysctl, dnsmasq, nftables snippets…"
|
||||||
|
write_sysctl
|
||||||
|
write_dnsmasq
|
||||||
|
write_nft
|
||||||
|
|
||||||
|
log "setting static IP on $IF_LAN…"
|
||||||
|
configure_eth_static
|
||||||
|
|
||||||
|
log "restarting dnsmasq…"
|
||||||
|
systemctl enable dnsmasq
|
||||||
|
systemctl restart dnsmasq
|
||||||
|
|
||||||
|
log "loading NAT rules and enabling nftables…"
|
||||||
|
apply_nft
|
||||||
|
systemctl enable nftables 2>/dev/null || true
|
||||||
|
systemctl restart nftables 2>/dev/null || true
|
||||||
|
|
||||||
|
log "done. Connect $IF_LAN to the external AP (DHCP off on the AP)."
|
||||||
|
log "Join Wi-Fi on $IF_WAN to the uplink network and complete any captive portal on the Pi."
|
||||||
|
}
|
||||||
|
|
||||||
|
do_remove() {
|
||||||
|
need_root
|
||||||
|
remove_eth_static
|
||||||
|
remove_dnsmasq
|
||||||
|
systemctl restart dnsmasq 2>/dev/null || true
|
||||||
|
|
||||||
|
remove_nft
|
||||||
|
systemctl restart nftables 2>/dev/null || true
|
||||||
|
|
||||||
|
remove_sysctl
|
||||||
|
sysctl -w net.ipv4.ip_forward=0 2>/dev/null || true
|
||||||
|
|
||||||
|
log "removed pi-eth-lan-router configuration snippets and NM profile '$NM_CON_NAME' (if present)."
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: sudo $0 install|remove
|
||||||
|
|
||||||
|
WAN (Wi-Fi client): $IF_WAN
|
||||||
|
LAN (Ethernet to AP): $IF_LAN
|
||||||
|
LAN address: ${LAN_IP}/${LAN_PREFIX}
|
||||||
|
DHCP range: $DHCP_START – $DHCP_END
|
||||||
|
|
||||||
|
Override with environment variables (see script header).
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
install) do_install ;;
|
||||||
|
remove) do_remove ;;
|
||||||
|
*) usage; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -124,15 +124,6 @@ def _register_ws(ip: str, ws) -> None:
|
|||||||
_send_locks[key] = asyncio.Lock()
|
_send_locks[key] = asyncio.Lock()
|
||||||
_schedule_status_broadcast(key, True)
|
_schedule_status_broadcast(key, True)
|
||||||
print(f"[WS] driver connected {key!r}")
|
print(f"[WS] driver connected {key!r}")
|
||||||
try:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
except RuntimeError:
|
|
||||||
return
|
|
||||||
|
|
||||||
async def _apply_saved_brightness():
|
|
||||||
await sync_global_brightness_to_driver(key)
|
|
||||||
|
|
||||||
loop.create_task(_apply_saved_brightness())
|
|
||||||
|
|
||||||
|
|
||||||
def unregister_tcp_writer(peer_ip: str, ws=None) -> str:
|
def unregister_tcp_writer(peer_ip: str, ws=None) -> str:
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class Zone(Model):
|
|||||||
"names": names if names else [],
|
"names": names if names else [],
|
||||||
"presets": presets if presets else [],
|
"presets": presets if presets else [],
|
||||||
"default_preset": None,
|
"default_preset": None,
|
||||||
|
"brightness": 255,
|
||||||
}
|
}
|
||||||
self.save()
|
self.save()
|
||||||
return next_id
|
return next_id
|
||||||
|
|||||||
@@ -17,24 +17,26 @@ function applyBrightnessSliders(val) {
|
|||||||
if (menuSlider) menuSlider.value = String(v);
|
if (menuSlider) menuSlider.value = String(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveGlobalBrightnessToServer(val) {
|
async function saveZoneBrightnessToServer(zoneId, val) {
|
||||||
|
if (!zoneId) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/settings/settings", {
|
const res = await fetch(`/zones/${zoneId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
body: JSON.stringify({ global_brightness: val }),
|
body: JSON.stringify({ brightness: val }),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({}));
|
const err = await res.json().catch(() => ({}));
|
||||||
console.warn("global_brightness save failed:", err.error || res.status);
|
console.warn("zone brightness save failed:", err.error || res.status);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("global_brightness save failed:", e);
|
console.warn("zone brightness save failed:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendZoneBrightness(value) {
|
function sendZoneBrightness(zoneId, value) {
|
||||||
|
if (!zoneId) return;
|
||||||
const val = Math.max(0, Math.min(255, parseInt(value, 10) || 0));
|
const val = Math.max(0, Math.min(255, parseInt(value, 10) || 0));
|
||||||
const headerSlider = document.getElementById('header-brightness-slider');
|
const headerSlider = document.getElementById('header-brightness-slider');
|
||||||
const menuSlider = document.getElementById('menu-brightness-slider');
|
const menuSlider = document.getElementById('menu-brightness-slider');
|
||||||
@@ -50,7 +52,7 @@ function sendZoneBrightness(value) {
|
|||||||
brightnessSendTimeout = setTimeout(() => {
|
brightnessSendTimeout = setTimeout(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await saveGlobalBrightnessToServer(val);
|
await saveZoneBrightnessToServer(zoneId, val);
|
||||||
const section = document.querySelector('.presets-section[data-zone-id]');
|
const section = document.querySelector('.presets-section[data-zone-id]');
|
||||||
const names = typeof window.parseTabDeviceNames === 'function'
|
const names = typeof window.parseTabDeviceNames === 'function'
|
||||||
? window.parseTabDeviceNames(section)
|
? window.parseTabDeviceNames(section)
|
||||||
@@ -550,11 +552,16 @@ async function loadZoneContent(zoneId) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Keep header and menu brightness controls in sync.
|
// Keep header and menu brightness controls in sync.
|
||||||
const brightnessSlider = document.getElementById('header-brightness-slider');
|
const zoneBrightness =
|
||||||
const menuBrightnessSlider = document.getElementById('menu-brightness-slider');
|
typeof zone.brightness === 'number'
|
||||||
if (menuBrightnessSlider && brightnessSlider) {
|
? zone.brightness
|
||||||
menuBrightnessSlider.value = brightnessSlider.value;
|
: parseInt(String(zone.brightness ?? ''), 10);
|
||||||
}
|
const normalizedBrightness = Number.isFinite(zoneBrightness)
|
||||||
|
? Math.max(0, Math.min(255, Math.round(zoneBrightness)))
|
||||||
|
: 255;
|
||||||
|
applyBrightnessSliders(normalizedBrightness);
|
||||||
|
// Apply this zone's saved brightness when switching zones.
|
||||||
|
sendZoneBrightness(zoneId, normalizedBrightness);
|
||||||
|
|
||||||
// Trigger presets loading if the function exists
|
// Trigger presets loading if the function exists
|
||||||
if (typeof renderTabPresets === 'function') {
|
if (typeof renderTabPresets === 'function') {
|
||||||
@@ -1025,38 +1032,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const menuBrightnessSlider = document.getElementById('menu-brightness-slider');
|
const menuBrightnessSlider = document.getElementById('menu-brightness-slider');
|
||||||
const headerBrightnessSlider = document.getElementById('header-brightness-slider');
|
const headerBrightnessSlider = document.getElementById('header-brightness-slider');
|
||||||
(async () => {
|
(async () => {
|
||||||
let fromServer = null;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/settings', {
|
|
||||||
headers: { Accept: 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
const g = data.global_brightness;
|
|
||||||
if (typeof g === 'number' && g >= 0 && g <= 255) {
|
|
||||||
fromServer = Math.round(g);
|
|
||||||
} else if (g != null && g !== '') {
|
|
||||||
const n = parseInt(String(g), 10);
|
|
||||||
if (!Number.isNaN(n) && n >= 0 && n <= 255) {
|
|
||||||
fromServer = n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
if (fromServer !== null) {
|
|
||||||
applyBrightnessSliders(fromServer);
|
|
||||||
}
|
|
||||||
if (menuBrightnessSlider) {
|
if (menuBrightnessSlider) {
|
||||||
menuBrightnessSlider.addEventListener('input', (e) => {
|
menuBrightnessSlider.addEventListener('input', (e) => {
|
||||||
sendZoneBrightness(e.target.value);
|
if (!currentZoneId) return;
|
||||||
|
sendZoneBrightness(currentZoneId, e.target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (headerBrightnessSlider) {
|
if (headerBrightnessSlider) {
|
||||||
headerBrightnessSlider.addEventListener('input', (e) => {
|
headerBrightnessSlider.addEventListener('input', (e) => {
|
||||||
sendZoneBrightness(e.target.value);
|
if (!currentZoneId) return;
|
||||||
|
sendZoneBrightness(currentZoneId, e.target.value);
|
||||||
});
|
});
|
||||||
sendZoneBrightness(headerBrightnessSlider.value);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user