Files

173 lines
7.2 KiB
JavaScript

import { api, airMeterPalette, airQuality, configureTheme, isDemo, modeInfo, numberOrDash, panelUrl, showToast } from "./api.js";
const requestedLayout = new URLSearchParams(window.location.search).get("layout");
const layoutMode = ["horizontal", "vertical"].includes(requestedLayout) ? requestedLayout : null;
const wideLayout = window.matchMedia("(min-width: 800px)");
function applyLayout() {
document.documentElement.dataset.widgetLayout = layoutMode || (wideLayout.matches ? "horizontal" : "vertical");
}
applyLayout();
wideLayout.addEventListener("change", () => !layoutMode && applyLayout());
const $ = selector => document.querySelector(selector);
const themeMode = configureTheme($("#theme-button"));
let state = null;
let busy = false;
if (isDemo) $("#demo-note").hidden = false;
function setText(selector, value) { $(selector).textContent = value; }
function setPill(element, label, tone) { element.textContent = label; element.className = `pill ${tone}`; }
function toneClass(tone) { return ["good", "warn", "bad"].includes(tone) ? tone : "muted"; }
function updateSpeedVisual(value) {
const speed = Math.max(1, Math.min(6, Number(value) || 1));
setText("#speed", speed);
$("#speed").classList.remove("skeleton");
document.querySelectorAll("[data-speed]").forEach(button => {
const segment = Number(button.dataset.speed);
button.classList.toggle("active", segment <= speed);
button.setAttribute("aria-pressed", String(segment === speed));
});
}
function updateTemperatureVisual(value) {
const temperature = Math.max(5, Math.min(30, Number(value) || 5));
setText("#target-temp", temperature);
$("#target-temp").classList.remove("skeleton");
$("#temperature-range").value = temperature;
}
function drawToggle(selector, active) {
const button = $(selector);
button.setAttribute("aria-pressed", String(Boolean(active)));
button.textContent = active ? "ON" : "OFF";
}
function render(status) {
state = status;
const tion = status.tion || {};
const sensor = status.qingping || {};
const schedule = status.schedule || {};
const mode = modeInfo(status);
const quality = airQuality(sensor.co2);
const headerMode = schedule.override_active && schedule.override_until_time
? `Ручной до ${schedule.override_until_time}`
: mode.label;
setPill($("#connection"), status.online ? "Online" : "Offline", status.online ? "good" : "bad");
setPill($("#mode-pill"), headerMode, mode.tone);
setText("#mode-detail", mode.detail);
updateSpeedVisual(tion.fan_speed);
setText("#speed-note", status.auto?.state === "active" && status.auto?.target_speed
? `AUTO выбрал скорость ${status.auto.target_speed}`
: `Ступень ${numberOrDash(tion.fan_speed)} из 6`);
updateTemperatureVisual(tion.target_temp);
setText("#heating-note", tion.heating ? "Сейчас нагревает входящий воздух" : tion.heater ? "Обогрев разрешён" : "Желаемая температура воздуха");
drawToggle("#power-toggle", tion.power);
drawToggle("#heater-toggle", tion.heater);
setText("#power-detail", tion.power ? "Устройство включено" : "Устройство выключено");
setText("#heater-detail", tion.heating ? "Сейчас нагревает" : tion.heater ? "Автоматически" : "Выключен");
setText("#co2", numberOrDash(sensor.co2));
setText("#air-label", quality.label);
const airTone = toneClass(quality.tone);
const airPalette = airMeterPalette(sensor.co2);
const airCard = $(".air-card");
$("#air-label").className = `quality-chip ${airTone}`;
airCard.dataset.airTone = airTone;
airCard.style.setProperty("--air-color", airPalette.color);
airCard.style.setProperty("--air-glow", airPalette.glow);
$("#co2-progress").style.width = `${Math.max(0, Math.min(100, quality.pct))}%`;
setText("#room-temp", numberOrDash(sensor.temperature ?? tion.in_temp, 1));
setText("#humidity", numberOrDash(sensor.humidity));
setText("#current-time", schedule.current_time || tion.device_time || "—");
setText("#next-time", schedule.next_time || "—");
}
async function refresh(silent = false) {
try { render(await api.status()); }
catch (error) {
setPill($("#connection"), "Ошибка", "bad");
if (!silent) showToast(error.message, true);
}
}
async function act(path, source) {
if (busy) return;
busy = true;
source?.setAttribute("disabled", "");
try { await api.post(path); await refresh(true); }
catch (error) { showToast(error.message, true); await refresh(true); }
finally { busy = false; source?.removeAttribute("disabled"); }
}
function stepped(kind, delta, source) {
if (busy || !state?.tion) return;
const isSpeed = kind === "speed";
const current = Number(isSpeed ? state.tion.fan_speed : state.tion.target_temp);
const min = isSpeed ? 1 : 5;
const max = isSpeed ? 6 : 30;
const value = Math.min(max, Math.max(min, current + delta));
if (!Number.isFinite(value) || value === current) return;
if (isSpeed) {
state.tion.fan_speed = value;
updateSpeedVisual(value);
} else {
state.tion.target_temp = value;
updateTemperatureVisual(value);
}
act(`/api/tion/${isSpeed ? "speed" : "temperature"}/${value}`, source);
}
document.querySelectorAll("[data-step]").forEach(button => button.addEventListener("click", () => {
stepped(button.dataset.step, Number(button.dataset.delta), button);
}));
document.querySelectorAll("[data-speed]").forEach(button => button.addEventListener("click", () => {
if (busy || !state?.tion) return;
const value = Number(button.dataset.speed);
state.tion.fan_speed = value;
updateSpeedVisual(value);
act(`/api/tion/speed/${value}`, button);
}));
$("#temperature-range").addEventListener("input", event => setText("#target-temp", event.target.value));
$("#temperature-range").addEventListener("change", event => {
if (busy || !state?.tion) return;
const value = Number(event.target.value);
state.tion.target_temp = value;
updateTemperatureVisual(value);
act(`/api/tion/temperature/${value}`, event.target);
});
$("#power-toggle").addEventListener("click", event => {
if (busy || !state?.tion) return;
const value = !state.tion.power;
state.tion.power = value;
drawToggle("#power-toggle", value);
setText("#power-detail", value ? "Устройство включено" : "Устройство выключено");
act(`/api/tion/power/${value ? "on" : "off"}`, event.currentTarget);
});
$("#heater-toggle").addEventListener("click", event => {
if (busy || !state?.tion) return;
const value = !state.tion.heater;
state.tion.heater = value;
drawToggle("#heater-toggle", value);
setText("#heater-detail", value ? "Автоматически" : "Выключен");
act(`/api/tion/heater/${value ? "on" : "off"}`, event.currentTarget);
});
$("#open-panel").addEventListener("click", () => window.open(panelUrl(themeMode()), "_blank", "noopener"));
$("#air-link").addEventListener("click", () => window.open(panelUrl(themeMode(), "air"), "_blank", "noopener"));
$("#schedule-link").addEventListener("click", () => window.open(panelUrl(themeMode(), "schedule"), "_blank", "noopener"));
refresh();
setInterval(() => !document.hidden && !busy && refresh(true), 3000);