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;
|
||||
const entities = [
|
||||
['sensor', 'dickendock_koepi_angebot', {
|
||||
name: 'KöPi bestes Angebot',
|
||||
name: 'KöPi Angebote',
|
||||
unique_id: 'dickendock_koepi_bestes_angebot',
|
||||
state_topic: `${BASE_TOPIC}/koepi/state`,
|
||||
json_attributes_topic: `${BASE_TOPIC}/koepi/attributes`,
|
||||
@@ -85,6 +85,24 @@ function publishDiscovery() {
|
||||
availability_topic: AVAILABILITY_TOPIC,
|
||||
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', {
|
||||
name: 'DickenDock Nutzer online',
|
||||
unique_id: 'dickendock_nutzer_online',
|
||||
@@ -108,14 +126,61 @@ function parsePrice(str) {
|
||||
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
|
||||
function publishKoepiState({ offers, changed }) {
|
||||
if (!client?.connected) return;
|
||||
const list = offers || [];
|
||||
const best = list.length
|
||||
? [...list].sort((a, b) => parsePrice(a.price) - parsePrice(b.price))[0]
|
||||
: null;
|
||||
const state = best ? `${best.retailer}: ${best.price}` : 'keine Angebote';
|
||||
const list = [...(offers || [])].sort((a, b) => parsePrice(a.price) - parsePrice(b.price));
|
||||
|
||||
// State: alle Angebote als kompakte Zeile, auf HA-State-Limit (255 Zeichen) gekürzt
|
||||
let state = list.length
|
||||
? 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/attributes`, JSON.stringify({
|
||||
@@ -124,6 +189,10 @@ function publishKoepiState({ offers, changed }) {
|
||||
letzter_check: new Date().toISOString(),
|
||||
}), { 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
|
||||
|
||||
Reference in New Issue
Block a user