SmartestHome/render/floorplan-3d/floorplan-app.js

95 lines
3.0 KiB
JavaScript

/*
* Wires floorplan3d.js to identity's GET /floorplan/presence.
*
* One request gives the plan AND who is in it, so there is no second call and no window
* where the rooms are drawn but the people are missing.
*/
"use strict";
const params = new URLSearchParams(location.search);
const API = (params.get("api") || "").replace(/\/$/, "");
const TOKEN = params.get("token") || "";
const POLL_MS = Number(params.get("poll") || 15000);
const $ = (id) => document.getElementById(id);
if (!API || !TOKEN) {
$("error").hidden = false;
$("error").textContent = "Not configured — needs ?api=&token= in the URL.";
throw new Error("floorplan: missing ?api=/&token=");
}
const plan = new Floorplan3D($("plan"), {
// identity serves the picture; this builds the URL rather than the SDK knowing about
// any particular service.
photoUrl: (personId) => `${API}/people/${personId}/photo?token=${encodeURIComponent(TOKEN)}`,
});
let levels = [];
let activeLevelId = params.get("level") ? Number(params.get("level")) : null;
function load() {
fetch(`${API}/floorplan/presence`, { headers: { Authorization: `Bearer ${TOKEN}` } })
.then((res) => {
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json();
})
.then((data) => {
$("error").hidden = true;
levels = data.levels || [];
if (activeLevelId === null && levels.length) activeLevelId = levels[0].id;
renderLevelButtons();
const level = levels.find((l) => l.id === activeLevelId) || levels[0] || null;
$("level-name").textContent = level ? level.name : "No levels drawn yet";
plan.setLevel(level, data.unplaced || []);
// The honest count, in the same sentence: placed people and the ones the system
// cannot locate. A view that silently omits the second number implies it knows
// where everybody is.
const placed = (level ? level.rooms || [] : []).reduce(
(n, room) => n + (room.occupants || []).length, 0
);
const unplaced = (data.unplaced || []).length;
$("summary").textContent = unplaced
? `${placed} placed · ${unplaced} home but not locatable`
: `${placed} placed`;
})
.catch((err) => {
$("error").hidden = false;
$("error").textContent = `identity: ${err.message}`;
})
.finally(() => setTimeout(load, POLL_MS));
}
function renderLevelButtons() {
if (levels.length < 2) {
$("levels").innerHTML = "";
return;
}
$("levels").innerHTML = levels
.map(
(l) =>
`<button data-id="${l.id}" class="${l.id === activeLevelId ? "active" : ""}">${escapeHtml(
l.name
)}</button>`
)
.join("");
$("levels")
.querySelectorAll("button")
.forEach((btn) =>
btn.addEventListener("click", () => {
activeLevelId = Number(btn.dataset.id);
load();
})
);
}
function escapeHtml(s) {
return String(s ?? "").replace(/[&<>"']/g, (c) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])
);
}
load();