work : добавил web в отслеживание
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { api, 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);
|
||||
$("#air-label").className = `quality-chip ${toneClass(quality.tone)}`;
|
||||
$("#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);
|
||||
Reference in New Issue
Block a user