selecta automat integration
This commit is contained in:
parent
0403ac21ef
commit
a4f1a01d24
@ -280,6 +280,138 @@
|
|||||||
white-space: pre-wrap;
|
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) {
|
@media (max-width: 1200px) {
|
||||||
.dashboard { grid-template-columns: repeat(2, minmax(260px, 1fr)); }
|
.dashboard { grid-template-columns: repeat(2, minmax(260px, 1fr)); }
|
||||||
}
|
}
|
||||||
@ -363,6 +495,29 @@
|
|||||||
<div id="matrix" class="matrix"></div>
|
<div id="matrix" class="matrix"></div>
|
||||||
<div id="ledStatus" class="led-status">Status: -</div>
|
<div id="ledStatus" class="led-status">Status: -</div>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
|
|
||||||
<div id="log" class="log">Bereit.</div>
|
<div id="log" class="log">Bereit.</div>
|
||||||
@ -375,6 +530,12 @@
|
|||||||
const TOPIC_LED_MATRIX = "MOBKOM/LED/set_matrix";
|
const TOPIC_LED_MATRIX = "MOBKOM/LED/set_matrix";
|
||||||
const TOPIC_LED_STATE = "MOBKOM/LED/state";
|
const TOPIC_LED_STATE = "MOBKOM/LED/state";
|
||||||
const TOPIC_LED_STATE_MATRIX = "MOBKOM/LED/state_matrix";
|
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 = {
|
const groups = {
|
||||||
gps: {
|
gps: {
|
||||||
@ -410,6 +571,9 @@
|
|||||||
},
|
},
|
||||||
led: {
|
led: {
|
||||||
filter: "MOBKOM/LED/state_matrix"
|
filter: "MOBKOM/LED/state_matrix"
|
||||||
|
},
|
||||||
|
selecta: {
|
||||||
|
filter: "MOBKOM/SELECTA/#"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -424,13 +588,19 @@
|
|||||||
const readAllBtn = document.getElementById("readAllBtn");
|
const readAllBtn = document.getElementById("readAllBtn");
|
||||||
const clearBtn = document.getElementById("clearBtn");
|
const clearBtn = document.getElementById("clearBtn");
|
||||||
const logBox = document.getElementById("log");
|
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 client = null;
|
||||||
let isPainting = false;
|
let isPainting = false;
|
||||||
let paintMode = "draw";
|
let paintMode = "draw";
|
||||||
let patternBusy = false;
|
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 valueEls = {};
|
||||||
const ledEls = [];
|
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) {
|
function updateSubButton(key) {
|
||||||
const btn = document.querySelector(`[data-sub="${key}"]`);
|
const btn = document.querySelector(`[data-sub="${key}"]`);
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
@ -707,6 +1011,9 @@
|
|||||||
client.subscribe(topic, { qos: 0 });
|
client.subscribe(topic, { qos: 0 });
|
||||||
subscribed[key] = true;
|
subscribed[key] = true;
|
||||||
log("Subscribed: " + topic);
|
log("Subscribed: " + topic);
|
||||||
|
if (key === "selecta") {
|
||||||
|
setTimeout(() => publish(TOPIC_SELECTA_GET_STATE, "ok"), 200);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
client.unsubscribe(topic);
|
client.unsubscribe(topic);
|
||||||
subscribed[key] = false;
|
subscribed[key] = false;
|
||||||
@ -743,6 +1050,10 @@
|
|||||||
const payload = message.toString();
|
const payload = message.toString();
|
||||||
if (topic === TOPIC_LED_STATE || topic === TOPIC_LED_STATE_MATRIX) {
|
if (topic === TOPIC_LED_STATE || topic === TOPIC_LED_STATE_MATRIX) {
|
||||||
handleLedState(payload);
|
handleLedState(payload);
|
||||||
|
} else if (topic === TOPIC_SELECTA_STATE) {
|
||||||
|
handleSelectaState(payload);
|
||||||
|
} else if (topic === TOPIC_SELECTA_WARNING) {
|
||||||
|
handleSelectaWarning(payload);
|
||||||
} else if (valueEls[topic]) {
|
} else if (valueEls[topic]) {
|
||||||
valueEls[topic].textContent = formatValue(payload);
|
valueEls[topic].textContent = formatValue(payload);
|
||||||
}
|
}
|
||||||
@ -775,6 +1086,10 @@
|
|||||||
disconnectBtn.addEventListener("click", disconnectMqtt);
|
disconnectBtn.addEventListener("click", disconnectMqtt);
|
||||||
clearBtn.addEventListener("click", clearMatrix);
|
clearBtn.addEventListener("click", clearMatrix);
|
||||||
readAllBtn.addEventListener("click", readAllPixels);
|
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]")) {
|
for (const btn of document.querySelectorAll("[data-sub]")) {
|
||||||
btn.addEventListener("click", () => toggleSubscribe(btn.dataset.sub));
|
btn.addEventListener("click", () => toggleSubscribe(btn.dataset.sub));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user