selecta automat integration
This commit is contained in:
parent
0403ac21ef
commit
a4f1a01d24
@ -280,6 +280,138 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.selecta-warning {
|
||||
padding: 10px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255,176,32,.12);
|
||||
border: 1px solid rgba(255,176,32,.28);
|
||||
color: #ffd28a;
|
||||
font-size: 13px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.selecta-warning.inactive {
|
||||
background: rgba(47,209,124,.10);
|
||||
border-color: rgba(47,209,124,.25);
|
||||
color: #b6f7d1;
|
||||
}
|
||||
|
||||
.warning-stack {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.warning-item {
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255,176,32,.13);
|
||||
border: 1px solid rgba(255,176,32,.32);
|
||||
color: #ffd28a;
|
||||
}
|
||||
|
||||
.warning-item.empty {
|
||||
background: rgba(255,92,92,.16);
|
||||
border-color: rgba(255,92,92,.45);
|
||||
color: #ffb7b7;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.drink-list {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.drink-row {
|
||||
display: grid;
|
||||
grid-template-columns: 18px 1fr auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background: rgba(0,0,0,.16);
|
||||
border: 1px solid rgba(255,255,255,.05);
|
||||
}
|
||||
|
||||
.drink-row.low {
|
||||
border-color: rgba(255,176,32,.32);
|
||||
background: rgba(255,176,32,.08);
|
||||
}
|
||||
|
||||
.drink-row.empty {
|
||||
border-color: rgba(255,92,92,.45);
|
||||
background: rgba(255,92,92,.10);
|
||||
}
|
||||
|
||||
.drink-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(255,255,255,.2);
|
||||
}
|
||||
|
||||
.drink-info {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.drink-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.drink-title .selected {
|
||||
color: var(--cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stockbar {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stockbar-fill {
|
||||
height: 100%;
|
||||
background: var(--cyan);
|
||||
border-radius: 999px;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.stockbar-fill.low {
|
||||
background: var(--orange) !important;
|
||||
}
|
||||
|
||||
.stockbar-fill.empty {
|
||||
background: var(--red) !important;
|
||||
}
|
||||
|
||||
.selecta-tools {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard { grid-template-columns: repeat(2, minmax(260px, 1fr)); }
|
||||
}
|
||||
@ -363,6 +495,29 @@
|
||||
<div id="matrix" class="matrix"></div>
|
||||
<div id="ledStatus" class="led-status">Status: -</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-title"><h2>Selecta Automat</h2><span>MOBKOM/SELECTA/#</span></div>
|
||||
<button class="sub-btn primary" data-sub="selecta">Abonnieren</button>
|
||||
</div>
|
||||
|
||||
<div id="selectaWarning" class="selecta-warning inactive">Keine Warnung.</div>
|
||||
|
||||
<div class="selecta-tools">
|
||||
<button id="selectaGetBtn">Status laden</button>
|
||||
<button id="selectaResetBtn" class="primary">Alle auffüllen</button>
|
||||
</div>
|
||||
|
||||
<div class="selecta-tools">
|
||||
<button id="selectaOnBtn">Automat aktivieren</button>
|
||||
<button id="selectaOffBtn" class="danger">Automat deaktivieren</button>
|
||||
</div>
|
||||
|
||||
<div id="drinkList" class="drink-list"></div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<div id="log" class="log">Bereit.</div>
|
||||
@ -375,6 +530,12 @@
|
||||
const TOPIC_LED_MATRIX = "MOBKOM/LED/set_matrix";
|
||||
const TOPIC_LED_STATE = "MOBKOM/LED/state";
|
||||
const TOPIC_LED_STATE_MATRIX = "MOBKOM/LED/state_matrix";
|
||||
const TOPIC_SELECTA_STATE = "MOBKOM/SELECTA/state";
|
||||
const TOPIC_SELECTA_WARNING = "MOBKOM/SELECTA/warning";
|
||||
const TOPIC_SELECTA_REFILL = "MOBKOM/SELECTA/refill";
|
||||
const TOPIC_SELECTA_RESET = "MOBKOM/SELECTA/reset";
|
||||
const TOPIC_SELECTA_GET_STATE = "MOBKOM/SELECTA/get_state";
|
||||
const TOPIC_SELECTA_ACTIVE = "MOBKOM/SELECTA/active";
|
||||
|
||||
const groups = {
|
||||
gps: {
|
||||
@ -410,6 +571,9 @@
|
||||
},
|
||||
led: {
|
||||
filter: "MOBKOM/LED/state_matrix"
|
||||
},
|
||||
selecta: {
|
||||
filter: "MOBKOM/SELECTA/#"
|
||||
}
|
||||
};
|
||||
|
||||
@ -424,13 +588,19 @@
|
||||
const readAllBtn = document.getElementById("readAllBtn");
|
||||
const clearBtn = document.getElementById("clearBtn");
|
||||
const logBox = document.getElementById("log");
|
||||
const selectaWarning = document.getElementById("selectaWarning");
|
||||
const drinkList = document.getElementById("drinkList");
|
||||
const selectaGetBtn = document.getElementById("selectaGetBtn");
|
||||
const selectaResetBtn = document.getElementById("selectaResetBtn");
|
||||
const selectaOnBtn = document.getElementById("selectaOnBtn");
|
||||
const selectaOffBtn = document.getElementById("selectaOffBtn");
|
||||
|
||||
let client = null;
|
||||
let isPainting = false;
|
||||
let paintMode = "draw";
|
||||
let patternBusy = false;
|
||||
|
||||
const subscribed = { gps: false, sens: false, imu: false, led: false };
|
||||
const subscribed = { gps: false, sens: false, imu: false, led: false, selecta: false };
|
||||
const valueEls = {};
|
||||
const ledEls = [];
|
||||
|
||||
@ -683,6 +853,140 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function rgbToCssSelecta(rgb) {
|
||||
return `rgb(${Number(rgb[0]) || 0}, ${Number(rgb[1]) || 0}, ${Number(rgb[2]) || 0})`;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function renderSelectaWarnings(warnings) {
|
||||
if (!warnings || warnings.length === 0) {
|
||||
selectaWarning.classList.add("inactive");
|
||||
selectaWarning.innerHTML = "Keine Warnung.";
|
||||
return;
|
||||
}
|
||||
|
||||
selectaWarning.classList.remove("inactive");
|
||||
selectaWarning.innerHTML = `
|
||||
<div class="warning-stack">
|
||||
${warnings.map(w => {
|
||||
const stock = Number(w.stock);
|
||||
const isEmpty = stock <= 0 || w.severity === "empty";
|
||||
const title = isEmpty ? "LEER" : "Tiefer Bestand";
|
||||
const name = escapeHtml(w.name || w.id || "Getränk");
|
||||
const message = escapeHtml(w.message || (isEmpty ? `${name} ist leer.` : `Nur noch ${stock}x ${name} übrig.`));
|
||||
return `
|
||||
<div class="warning-item ${isEmpty ? "empty" : ""}">
|
||||
<div class="warning-title">
|
||||
<span>${title}: ${name}</span>
|
||||
<span>${Number.isFinite(stock) ? stock : "?"}</span>
|
||||
</div>
|
||||
<div class="warning-message">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function handleSelectaWarning(payload) {
|
||||
try {
|
||||
const data = JSON.parse(payload);
|
||||
|
||||
if (Array.isArray(data.warnings)) {
|
||||
renderSelectaWarnings(data.active ? data.warnings : []);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.active) {
|
||||
renderSelectaWarnings([data]);
|
||||
} else {
|
||||
renderSelectaWarnings([]);
|
||||
}
|
||||
} catch (_) {
|
||||
renderSelectaWarnings([{ name: "Warnung", stock: NaN, message: payload, severity: "low" }]);
|
||||
}
|
||||
}
|
||||
|
||||
function refillSelecta(id) {
|
||||
publish(TOPIC_SELECTA_REFILL, JSON.stringify({ id: id, stock: 8 }));
|
||||
}
|
||||
|
||||
function handleSelectaState(payload) {
|
||||
try {
|
||||
const data = JSON.parse(payload);
|
||||
drinkList.innerHTML = "";
|
||||
|
||||
for (const d of data.drinks || []) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "drink-row";
|
||||
|
||||
const stock = Number(d.stock);
|
||||
const max = Number(d.max) || 8;
|
||||
const warningLimit = Number(data.warning_limit) || 3;
|
||||
if (stock <= 0) row.classList.add("empty");
|
||||
else if (stock <= warningLimit) row.classList.add("low");
|
||||
|
||||
const color = document.createElement("div");
|
||||
color.className = "drink-color";
|
||||
color.style.background = rgbToCssSelecta(d.color || [0,0,0]);
|
||||
|
||||
const info = document.createElement("div");
|
||||
info.className = "drink-info";
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.className = "drink-title";
|
||||
title.innerHTML = `<span class="${d.selected ? "selected" : ""}">${d.selected ? "▶ " : ""}${escapeHtml(d.name)}</span><span>${stock}/${max}</span>`;
|
||||
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "stockbar";
|
||||
const fill = document.createElement("div");
|
||||
fill.className = "stockbar-fill";
|
||||
fill.style.width = `${Math.max(0, Math.min(100, (stock / max) * 100))}%`;
|
||||
fill.style.background = rgbToCssSelecta(d.color || [0,0,0]);
|
||||
if (stock <= 0) fill.classList.add("empty");
|
||||
else if (stock <= warningLimit) fill.classList.add("low");
|
||||
bar.appendChild(fill);
|
||||
|
||||
info.appendChild(title);
|
||||
info.appendChild(bar);
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.textContent = "Auffüllen";
|
||||
btn.onclick = () => refillSelecta(d.id);
|
||||
|
||||
row.appendChild(color);
|
||||
row.appendChild(info);
|
||||
row.appendChild(btn);
|
||||
drinkList.appendChild(row);
|
||||
}
|
||||
|
||||
const stateWarnings = (data.drinks || [])
|
||||
.filter(d => Number(d.stock) <= (Number(data.warning_limit) || 3))
|
||||
.map(d => ({
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
stock: Number(d.stock),
|
||||
limit: Number(data.warning_limit) || 3,
|
||||
severity: Number(d.stock) <= 0 ? "empty" : "low",
|
||||
message: Number(d.stock) <= 0
|
||||
? `${d.name} ist leer.`
|
||||
: `Nur noch ${d.stock}x ${d.name} übrig.`
|
||||
}));
|
||||
renderSelectaWarnings(stateWarnings);
|
||||
} catch (e) {
|
||||
log("Selecta state konnte nicht gelesen werden: " + payload);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSubButton(key) {
|
||||
const btn = document.querySelector(`[data-sub="${key}"]`);
|
||||
if (!btn) return;
|
||||
@ -707,6 +1011,9 @@
|
||||
client.subscribe(topic, { qos: 0 });
|
||||
subscribed[key] = true;
|
||||
log("Subscribed: " + topic);
|
||||
if (key === "selecta") {
|
||||
setTimeout(() => publish(TOPIC_SELECTA_GET_STATE, "ok"), 200);
|
||||
}
|
||||
} else {
|
||||
client.unsubscribe(topic);
|
||||
subscribed[key] = false;
|
||||
@ -743,6 +1050,10 @@
|
||||
const payload = message.toString();
|
||||
if (topic === TOPIC_LED_STATE || topic === TOPIC_LED_STATE_MATRIX) {
|
||||
handleLedState(payload);
|
||||
} else if (topic === TOPIC_SELECTA_STATE) {
|
||||
handleSelectaState(payload);
|
||||
} else if (topic === TOPIC_SELECTA_WARNING) {
|
||||
handleSelectaWarning(payload);
|
||||
} else if (valueEls[topic]) {
|
||||
valueEls[topic].textContent = formatValue(payload);
|
||||
}
|
||||
@ -775,6 +1086,10 @@
|
||||
disconnectBtn.addEventListener("click", disconnectMqtt);
|
||||
clearBtn.addEventListener("click", clearMatrix);
|
||||
readAllBtn.addEventListener("click", readAllPixels);
|
||||
selectaGetBtn.addEventListener("click", () => publish(TOPIC_SELECTA_GET_STATE, "ok"));
|
||||
selectaResetBtn.addEventListener("click", () => publish(TOPIC_SELECTA_RESET, "ok"));
|
||||
selectaOnBtn.addEventListener("click", () => publish(TOPIC_SELECTA_ACTIVE, "on"));
|
||||
selectaOffBtn.addEventListener("click", () => publish(TOPIC_SELECTA_ACTIVE, "off"));
|
||||
|
||||
for (const btn of document.querySelectorAll("[data-sub]")) {
|
||||
btn.addEventListener("click", () => toggleSubscribe(btn.dataset.sub));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user