173 lines
7.2 KiB
JavaScript
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);
|