feat: HA-Entitaeten fuer Media-Anfragen, offene Bestellungen und 3D-Druck-Monatsumsatz

This commit is contained in:
2026-07-10 19:38:28 +02:00
parent 8a3b6ab912
commit 12094901a4
2 changed files with 120 additions and 1 deletions

View File

@@ -289,6 +289,9 @@ setTimeout(() => {
async function runScheduler() { async function runScheduler() {
mqttClient.publishPresence(); mqttClient.publishPresence();
mqttClient.publishMediaAnfragen();
mqttClient.publishBestellungen();
mqttClient.publishDruckStatistik();
try { try {
const now = db.prepare("SELECT datetime('now','localtime') as t").get().t; const now = db.prepare("SELECT datetime('now','localtime') as t").get().t;
const due = db.prepare(` const due = db.prepare(`

View File

@@ -114,6 +114,40 @@ function publishDiscovery() {
availability_topic: AVAILABILITY_TOPIC, availability_topic: AVAILABILITY_TOPIC,
device: DEVICE, device: DEVICE,
}], }],
['sensor', 'dickendock_media_anfragen', {
name: 'Media Anfragen offen',
unique_id: 'dickendock_media_anfragen',
state_topic: `${BASE_TOPIC}/media/anfragen`,
json_attributes_topic: `${BASE_TOPIC}/media/anfragen_attributes`,
icon: 'mdi:movie-open-star',
unit_of_measurement: 'Anfragen',
state_class: 'measurement',
availability_topic: AVAILABILITY_TOPIC,
device: DEVICE,
}],
['sensor', 'dickendock_bestellungen_offen', {
name: 'Bestellungen offen',
unique_id: 'dickendock_bestellungen_offen',
state_topic: `${BASE_TOPIC}/bestellungen/offen`,
json_attributes_topic: `${BASE_TOPIC}/bestellungen/attributes`,
icon: 'mdi:cube-send',
unit_of_measurement: 'Bestellungen',
state_class: 'measurement',
availability_topic: AVAILABILITY_TOPIC,
device: DEVICE,
}],
['sensor', 'dickendock_3ddruck_umsatz_monat', {
name: '3D-Druck Umsatz (Monat)',
unique_id: 'dickendock_3ddruck_umsatz_monat',
state_topic: `${BASE_TOPIC}/druck/umsatz_monat`,
json_attributes_topic: `${BASE_TOPIC}/druck/attributes`,
icon: 'mdi:printer-3d',
device_class: 'monetary',
unit_of_measurement: '€',
state_class: 'measurement',
availability_topic: AVAILABILITY_TOPIC,
device: DEVICE,
}],
]; ];
for (const [component, objectId, payload] of entities) { for (const [component, objectId, payload] of entities) {
client.publish(`homeassistant/${component}/${objectId}/config`, JSON.stringify(payload), { retain: true }); client.publish(`homeassistant/${component}/${objectId}/config`, JSON.stringify(payload), { retain: true });
@@ -207,7 +241,89 @@ function publishPresence() {
}), { retain: true }); }), { retain: true });
} }
// Wird jede Minute vom Scheduler aufgerufen — offene (unquittierte) Media-Favoriten,
// systemweit über alle Nutzer (spiegelt die Admin-Ansicht in "Media/Favoriten")
function publishMediaAnfragen() {
if (!client?.connected) return;
const offen = db.prepare('SELECT COUNT(*) AS n FROM movie_favorites WHERE acknowledged=0').get().n;
const liste = db.prepare(`
SELECT f.title, u.username, f.added_at
FROM movie_favorites f JOIN users u ON u.id = f.user_id
WHERE f.acknowledged = 0
ORDER BY f.added_at DESC LIMIT 20
`).all();
client.publish(`${BASE_TOPIC}/media/anfragen`, String(offen), { retain: true });
client.publish(`${BASE_TOPIC}/media/anfragen_attributes`, JSON.stringify({ anfragen: liste }), { retain: true });
}
// Wird jede Minute vom Scheduler aufgerufen — offene 3D-Druck-Bestellungen,
// systemweit über alle Nutzer, Status-Verteilung als Attribut
function publishBestellungen() {
if (!client?.connected) return;
const orders = db.prepare('SELECT status, bezahlt, abgeholt FROM orders').all();
const byStatus = { warteliste: 0, in_arbeit: 0, fertig: 0, bezahlt: 0, abgeschlossen: 0 };
for (const o of orders) {
if (o.bezahlt && o.abgeholt) byStatus.abgeschlossen++;
else if (o.bezahlt) byStatus.bezahlt++;
else if (o.status in byStatus) byStatus[o.status]++;
}
const offen = orders.length - byStatus.abgeschlossen;
client.publish(`${BASE_TOPIC}/bestellungen/offen`, String(offen), { retain: true });
client.publish(`${BASE_TOPIC}/bestellungen/attributes`, JSON.stringify({
...byStatus,
gesamt: orders.length,
}), { retain: true });
}
function round2(n) { return Math.round((n || 0) * 100) / 100; }
// Umsatz eines einzelnen Auftrags — Festpreis falls gesetzt, sonst Summe der
// Positions-Festpreise (spiegelt die Logik aus statistik/routes.js)
function getOrderRevenue(order) {
if (order.custom_price != null && order.custom_price > 0) return parseFloat(order.custom_price);
const r = db.prepare(`
SELECT COALESCE(SUM(custom_price * stueckzahl), 0) AS s
FROM order_items WHERE order_id=? AND custom_price IS NOT NULL
`).get(order.id);
return r?.s || 0;
}
// Wird jede Minute vom Scheduler aufgerufen — 3D-Druck-Umsatz im laufenden Kalendermonat,
// systemweit über alle Nutzer (bezahlte Aufträge nach bezahlt_am, sonst created_at)
function publishDruckStatistik() {
if (!client?.connected) return;
const now = new Date();
const pad = n => String(n).padStart(2, '0');
const monatsAnfang = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-01`;
const heute = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
const allOrders = db.prepare('SELECT * FROM orders').all();
const bezahltDiesenMonat = allOrders.filter(o => {
if (!o.bezahlt) return false;
const d = (o.bezahlt_am || o.created_at || '').slice(0, 10);
return d >= monatsAnfang && d <= heute;
});
const umsatzMonat = bezahltDiesenMonat.reduce((s, o) => s + getOrderRevenue(o), 0);
const ausgabenMonat = db.prepare(`
SELECT COALESCE(SUM(amount), 0) AS s FROM expenses WHERE date>=? AND date<=?
`).get(monatsAnfang, heute).s;
const offeneAuftraege = allOrders.filter(o => !(o.bezahlt && o.abgeholt)).length;
client.publish(`${BASE_TOPIC}/druck/umsatz_monat`, String(round2(umsatzMonat)), { retain: true });
client.publish(`${BASE_TOPIC}/druck/attributes`, JSON.stringify({
ausgaben_monat: round2(ausgabenMonat),
netto_monat: round2(umsatzMonat - ausgabenMonat),
bezahlte_auftraege_monat: bezahltDiesenMonat.length,
offene_auftraege_gesamt: offeneAuftraege,
}), { retain: true });
}
// Wird von index.js gesetzt, damit der HA-Button ohne Require-Zirkel den Check auslösen kann // Wird von index.js gesetzt, damit der HA-Button ohne Require-Zirkel den Check auslösen kann
function setKoepiCheckHandler(fn) { koepiCheckHandler = fn; } function setKoepiCheckHandler(fn) { koepiCheckHandler = fn; }
module.exports = { connectMqtt, publishKoepiState, publishPresence, setKoepiCheckHandler }; module.exports = {
connectMqtt, publishKoepiState, publishPresence, setKoepiCheckHandler,
publishMediaAnfragen, publishBestellungen, publishDruckStatistik,
};