feat: KöPi HA-Button sendet immer Pushover, Angebote-Sensor zeigt alle Angebote, neue Start/Ende-Datum-Sensoren
This commit is contained in:
@@ -59,7 +59,7 @@ function publishDiscovery() {
|
|||||||
if (!client) return;
|
if (!client) return;
|
||||||
const entities = [
|
const entities = [
|
||||||
['sensor', 'dickendock_koepi_angebot', {
|
['sensor', 'dickendock_koepi_angebot', {
|
||||||
name: 'KöPi bestes Angebot',
|
name: 'KöPi Angebote',
|
||||||
unique_id: 'dickendock_koepi_bestes_angebot',
|
unique_id: 'dickendock_koepi_bestes_angebot',
|
||||||
state_topic: `${BASE_TOPIC}/koepi/state`,
|
state_topic: `${BASE_TOPIC}/koepi/state`,
|
||||||
json_attributes_topic: `${BASE_TOPIC}/koepi/attributes`,
|
json_attributes_topic: `${BASE_TOPIC}/koepi/attributes`,
|
||||||
@@ -85,6 +85,24 @@ function publishDiscovery() {
|
|||||||
availability_topic: AVAILABILITY_TOPIC,
|
availability_topic: AVAILABILITY_TOPIC,
|
||||||
device: DEVICE,
|
device: DEVICE,
|
||||||
}],
|
}],
|
||||||
|
['sensor', 'dickendock_koepi_start', {
|
||||||
|
name: 'KöPi Angebot Start',
|
||||||
|
unique_id: 'dickendock_koepi_start',
|
||||||
|
state_topic: `${BASE_TOPIC}/koepi/start`,
|
||||||
|
device_class: 'date',
|
||||||
|
icon: 'mdi:calendar-start',
|
||||||
|
availability_topic: AVAILABILITY_TOPIC,
|
||||||
|
device: DEVICE,
|
||||||
|
}],
|
||||||
|
['sensor', 'dickendock_koepi_ende', {
|
||||||
|
name: 'KöPi Angebot Ende',
|
||||||
|
unique_id: 'dickendock_koepi_ende',
|
||||||
|
state_topic: `${BASE_TOPIC}/koepi/ende`,
|
||||||
|
device_class: 'date',
|
||||||
|
icon: 'mdi:calendar-end',
|
||||||
|
availability_topic: AVAILABILITY_TOPIC,
|
||||||
|
device: DEVICE,
|
||||||
|
}],
|
||||||
['sensor', 'dickendock_online', {
|
['sensor', 'dickendock_online', {
|
||||||
name: 'DickenDock Nutzer online',
|
name: 'DickenDock Nutzer online',
|
||||||
unique_id: 'dickendock_nutzer_online',
|
unique_id: 'dickendock_nutzer_online',
|
||||||
@@ -108,14 +126,61 @@ function parsePrice(str) {
|
|||||||
return Number.isNaN(n) ? Infinity : n;
|
return Number.isNaN(n) ? Infinity : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parst ein deutsches Datum "10.07." oder "10.07.2026" aus einem Textschnipsel.
|
||||||
|
// Fehlt das Jahr, wird das aktuelle Jahr angenommen (mit Jahreswechsel-Korrektur
|
||||||
|
// in beide Richtungen, z.B. bei Angeboten die über Silvester laufen).
|
||||||
|
function parseGermanDate(str, ref = new Date()) {
|
||||||
|
const m = String(str).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})?/);
|
||||||
|
if (!m) return null;
|
||||||
|
const day = parseInt(m[1], 10);
|
||||||
|
const month = parseInt(m[2], 10) - 1;
|
||||||
|
let year = m[3] ? parseInt(m[3], 10) : ref.getFullYear();
|
||||||
|
const d = new Date(year, month, day);
|
||||||
|
if (!m[3]) {
|
||||||
|
const diffMonths = (d.getFullYear() - ref.getFullYear()) * 12 + (d.getMonth() - ref.getMonth());
|
||||||
|
if (diffMonths < -3) d.setFullYear(year + 1); // z.B. Ref=Dez, Datum=Jan → nächstes Jahr gemeint
|
||||||
|
else if (diffMonths > 3) d.setFullYear(year - 1); // z.B. Ref=Jan, Datum=Dez → vergangenes Jahr gemeint
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ermittelt aus allen Angeboten den frühesten Start- und spätesten End-Termin
|
||||||
|
// (marktguru-Angebote für ein Produkt laufen i.d.R. im selben Wochenzeitraum)
|
||||||
|
function extractDateRange(offers) {
|
||||||
|
let start = null, end = null;
|
||||||
|
for (const o of offers) {
|
||||||
|
const text = o.dateRange || '';
|
||||||
|
const found = [...text.matchAll(/\d{1,2}\.\d{1,2}\.(?:\d{4})?/g)].map(m => m[0]);
|
||||||
|
if (found.length >= 2) {
|
||||||
|
const d1 = parseGermanDate(found[0]);
|
||||||
|
const d2 = parseGermanDate(found[1]);
|
||||||
|
if (d1 && (!start || d1 < start)) start = d1;
|
||||||
|
if (d2 && (!end || d2 > end)) end = d2;
|
||||||
|
} else if (found.length === 1) {
|
||||||
|
// Nur ein Datum im Text gefunden → als Enddatum werten
|
||||||
|
const d1 = parseGermanDate(found[0]);
|
||||||
|
if (d1 && (!end || d1 > end)) end = d1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIsoDate(d) {
|
||||||
|
if (!d) return null;
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Wird von koepi/routes.js nach jedem Check (Cron oder manueller Test) aufgerufen
|
// Wird von koepi/routes.js nach jedem Check (Cron oder manueller Test) aufgerufen
|
||||||
function publishKoepiState({ offers, changed }) {
|
function publishKoepiState({ offers, changed }) {
|
||||||
if (!client?.connected) return;
|
if (!client?.connected) return;
|
||||||
const list = offers || [];
|
const list = [...(offers || [])].sort((a, b) => parsePrice(a.price) - parsePrice(b.price));
|
||||||
const best = list.length
|
|
||||||
? [...list].sort((a, b) => parsePrice(a.price) - parsePrice(b.price))[0]
|
// State: alle Angebote als kompakte Zeile, auf HA-State-Limit (255 Zeichen) gekürzt
|
||||||
: null;
|
let state = list.length
|
||||||
const state = best ? `${best.retailer}: ${best.price}` : 'keine Angebote';
|
? list.map(o => `${o.retailer}: ${o.price}`).join(' | ')
|
||||||
|
: 'keine Angebote';
|
||||||
|
if (state.length > 255) state = state.slice(0, 252) + '…';
|
||||||
|
|
||||||
client.publish(`${BASE_TOPIC}/koepi/state`, state, { retain: true });
|
client.publish(`${BASE_TOPIC}/koepi/state`, state, { retain: true });
|
||||||
client.publish(`${BASE_TOPIC}/koepi/attributes`, JSON.stringify({
|
client.publish(`${BASE_TOPIC}/koepi/attributes`, JSON.stringify({
|
||||||
@@ -124,6 +189,10 @@ function publishKoepiState({ offers, changed }) {
|
|||||||
letzter_check: new Date().toISOString(),
|
letzter_check: new Date().toISOString(),
|
||||||
}), { retain: true });
|
}), { retain: true });
|
||||||
client.publish(`${BASE_TOPIC}/koepi/changed`, changed ? 'ON' : 'OFF', { retain: true });
|
client.publish(`${BASE_TOPIC}/koepi/changed`, changed ? 'ON' : 'OFF', { retain: true });
|
||||||
|
|
||||||
|
const { start, end } = extractDateRange(list);
|
||||||
|
client.publish(`${BASE_TOPIC}/koepi/start`, toIsoDate(start) || 'unknown', { retain: true });
|
||||||
|
client.publish(`${BASE_TOPIC}/koepi/ende`, toIsoDate(end) || 'unknown', { retain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wird jede Minute vom Scheduler in index.js aufgerufen
|
// Wird jede Minute vom Scheduler in index.js aufgerufen
|
||||||
|
|||||||
Reference in New Issue
Block a user