Ich habe mit copilot einen Weg gesucht, dass bei LIVE daten nicht ständig die Batterien getauscht werden müssen, Deshalb habe ich es so gelöst:
Generell ist LIVE an, aber nach erfolgreicher Datenübermittlung stellt sich LIVE auf false und wartet 5 Minuten. Dan geht er wieder online übermittelt und geht wieder für 5 Minuten schlafen. Das sollte reichen, um die Batterie zu schonen. Man kann auch kürze Intervalle einstellen.
// Abstand zwischen Live-Aktivierungen
liveCycleIntervalMinutes: 5,
Einstellen was ihr mögt.
Vielleicht kann es ja jemand gebrauchen.
/*
* OBI Energy Tracker
* Batteriesparmodus mit zyklischer Live-Aktualisierung
*
* Live wird kurzzeitig aktiviert und nach Empfang
* eines Datensatzes wieder deaktiviert.
*
* Benötigte Module:
* axios
* ws
*/
const CONFIG = {
email: "deine mail",
password: "dein passwort",
hhId: "OBI-Bridge",
midId: "OBI-Tracker",
stateRoot: "0_userdata.0.obi-energy",
debug: false,
// Historische Daten
pollCron: "*/5 * * * *",
historicalDurationMinutes: 15,
// Zyklischer Live-Betrieb
liveCycleEnabled: true,
// Sensorintervall während Live
liveUploadIntervalSeconds: 2,
// Sensorintervall im Batteriesparmodus
normalUploadIntervalSeconds: 300,
// Abstand zwischen Live-Aktivierungen
liveCycleIntervalMinutes: 5,
// Maximale Dauer eines Live-Versuchs
liveAttemptTimeoutSeconds: 45,
liveWriteMinIntervalMs: 2000,
liveReconnectDelayMs: 10000,
liveStaleAfterMs: 90000
};
// ============================================================================
// MODULE
// ============================================================================
const axios = require("axios");
const WebSocket = require("ws");
// ============================================================================
// API
// ============================================================================
const LOGIN_URL =
"https://www.obi.de/regi/auth/api/public/login";
const API_BASE_URL =
"https://energy-tracking-backend.prod-eks.dbs.obi.solutions";
const HISTORICAL_URL =
`${API_BASE_URL}/historical-data`;
const SENSOR_URL =
`${API_BASE_URL}/sensors`;
const LIVE_DATA_URL =
"wss://energy-tracking-livemode.prod-eks.dbs.obi.solutions/retrieving";
const API_KEY =
"Rh57q3vtOPYTf6FtArVN1boy2AyEiIqaGEmnMks7";
const USER_AGENT =
"heyOBI APP / iPhone17,2 / 4.9.1 / 560";
const LIVE_USER_AGENT = "app_client";
const ACCEPT_LANGUAGE = "de-DE,de;q=0.9";
const ACCEPT_HISTORICAL =
"application/vnd.obi.companion.energy-tracking.historical-record.v1+json";
const ACCEPT_SENSOR =
"application/vnd.obi.companion.energy-tracking.sensor.v1+json";
const TOKEN_MAX_AGE_MS = 55 * 60 * 1000;
const REQUEST_TIMEOUT_MS = 30000;
// ============================================================================
// VARIABLES
// ============================================================================
let token = null;
let tokenObtainedAt = 0;
let loginPromise = null;
let pollRunning = false;
let stopping = false;
let liveSocket = null;
let liveSocketConnecting = false;
let liveReconnectTimer = null;
let liveWatchdogTimer = null;
let liveCycleTimer = null;
let liveAttemptTimer = null;
let liveCycleActive = false;
let liveMessageReceived = false;
let lastLiveMessageAt = 0;
let lastLiveStateWriteAt = 0;
const http = axios.create({
timeout: REQUEST_TIMEOUT_MS,
validateStatus: () => true
});
// ============================================================================
// HILFSFUNKTIONEN
// ============================================================================
function stateId(name) {
return `${CONFIG.stateRoot}.${name}`;
}
function truncate(value, length = 500) {
const text = String(value ?? "");
return text.length <= length
? text
: text.substring(0, length) + "...";
}
function getErrorMessage(error) {
if (error?.response) {
return "HTTP " +
error.response.status +
": " +
truncate(JSON.stringify(error.response.data));
}
return error?.message || String(error);
}
function debugLog(message) {
if (CONFIG.debug) {
log("OBI DEBUG: " + message, "debug");
}
}
function berlinTime() {
return new Date().toLocaleString("de-DE", {
timeZone: "Europe/Berlin"
});
}
// ============================================================================
// KONFIGURATION PRÜFEN
// ============================================================================
function validateConfiguration() {
if (
!CONFIG.email ||
!CONFIG.password
) {
throw new Error("OBI Zugangsdaten konfigurieren.");
}
if (
!CONFIG.hhId ||
!CONFIG.midId
) {
throw new Error("OBI HH_ID und MID_ID konfigurieren.");
}
}
// ============================================================================
// LOGIN
// ============================================================================
function tokenIsStale() {
return (
!token ||
!tokenObtainedAt ||
Date.now() - tokenObtainedAt > TOKEN_MAX_AGE_MS
);
}
async function performLogin() {
validateConfiguration();
const response = await http.post(
LOGIN_URL,
{
password: CONFIG.password,
country: "de",
email: CONFIG.email
},
{
headers: {
"Content-Type": "application/json",
Accept: "*/*",
"User-Agent": USER_AGENT,
"Accept-Language": ACCEPT_LANGUAGE,
Cookie: "obi_storeid=527",
Origin: "https://www.obi.de",
Referer: "https://www.obi.de/"
}
}
);
if (response.status < 200 || response.status >= 300) {
throw new Error(
"OBI Login fehlgeschlagen: " + response.status
);
}
if (!response.data?.token) {
throw new Error("OBI Token fehlt.");
}
token = response.data.token;
tokenObtainedAt = Date.now();
log("OBI Energy: Login erfolgreich.", "info");
}
async function ensureLoggedIn(
forceRefresh = false,
failedToken = null
) {
if (stopping) {
throw new Error("Login während Stop verhindert.");
}
if (!forceRefresh && !tokenIsStale()) {
return token;
}
if (
forceRefresh &&
failedToken &&
token &&
token !== failedToken &&
!tokenIsStale()
) {
return token;
}
if (loginPromise) {
await loginPromise;
return token;
}
loginPromise = performLogin();
try {
await loginPromise;
return token;
} finally {
loginPromise = null;
}
}
// ============================================================================
// API HEADER
// ============================================================================
function apiHeaders(accept) {
return {
Authorization: `Bearer ${token}`,
"X-Api-Key": API_KEY,
"X-App-Type": "b2c",
Accept: accept,
"Accept-Language": ACCEPT_LANGUAGE,
"User-Agent": USER_AGENT,
"Cache-Control": "no-cache",
Pragma: "no-cache"
};
}
// ============================================================================
// AUTHENTICATED GET
// ============================================================================
async function authenticatedGet(
url,
accept,
retry = true
) {
await ensureLoggedIn();
const requestToken = token;
const response = await http.get(url, {
headers: apiHeaders(accept)
});
if (response.status === 401 && retry) {
await ensureLoggedIn(true, requestToken);
return authenticatedGet(url, accept, false);
}
if (response.status < 200 || response.status >= 300) {
throw new Error(
"OBI API Fehler " +
response.status +
" - " +
truncate(JSON.stringify(response.data))
);
}
return response.data;
}
// ============================================================================
// SENSOR UPLOAD INTERVALL SETZEN
// ============================================================================
async function setSensorUploadInterval(
midId,
uploadInterval
) {
if (stopping) {
throw new Error("Sensorintervall-Änderung während Stop verhindert.");
}
await ensureLoggedIn();
const failedToken = token;
const payload = {
id: midId,
uploadInterval: uploadInterval
};
const headers = {
Authorization: `Bearer ${token}`,
Accept: ACCEPT_SENSOR,
"Content-Type": ACCEPT_SENSOR,
"User-Agent": LIVE_USER_AGENT,
"X-Platform": "iOS",
"X-Lib-Version": "26.6.9"
};
let response = await http.patch(
`${SENSOR_URL}/${encodeURIComponent(midId)}`,
payload,
{ headers }
);
if (response.status === 401) {
await ensureLoggedIn(true, failedToken);
response = await http.patch(
`${SENSOR_URL}/${encodeURIComponent(midId)}`,
payload,
{
headers: {
...headers,
Authorization: `Bearer ${token}`
}
}
);
}
if (response.status < 200 || response.status >= 300) {
throw new Error(
"Sensorintervall konnte nicht gesetzt werden: " +
response.status +
" - " +
truncate(JSON.stringify(response.data))
);
}
await setStateAsync(stateId("live_upload_interval"), {
val: uploadInterval,
ack: true
});
log(
"OBI Energy: Sensorintervall auf " +
uploadInterval +
" Sekunden gesetzt.",
"info"
);
}
// ============================================================================
// HISTORISCHE DATEN
// ============================================================================
function isoTimestampMinutesAgo(minutes) {
return new Date(
Date.now() - minutes * 60000
).toISOString();
}
async function getHistoricalData(hhId, midId) {
const url =
`${HISTORICAL_URL}/` +
`${encodeURIComponent(hhId)}/` +
`${encodeURIComponent(midId)}/meter`;
const start = isoTimestampMinutesAgo(
CONFIG.historicalDurationMinutes
);
const duration =
`PT${CONFIG.historicalDurationMinutes}M`;
const query =
`duration=${encodeURIComponent(start + "/" + duration)}` +
`&measures=${encodeURIComponent("energy,negative_energy")}`;
return authenticatedGet(
`${url}?${query}`,
ACCEPT_HISTORICAL
);
}
// ============================================================================
// HISTORISCHE DATEN AUSWERTEN
// ============================================================================
function normalizeHistoricalRecords(data) {
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.measurements)) return data.measurements;
if (Array.isArray(data?.data)) return data.data;
if (Array.isArray(data?.results)) return data.results;
return [];
}
function measurementTimestamp(record) {
return (
record?.time ??
record?.timestamp ??
record?.dateTime ??
record?.datetime ??
""
);
}
function latestMeasurement(records, measure) {
const result = records.filter(item =>
item &&
item.measure === measure &&
item.value !== null &&
item.value !== undefined
);
if (!result.length) return null;
result.sort((a, b) =>
String(measurementTimestamp(a)).localeCompare(
String(measurementTimestamp(b))
)
);
return result[result.length - 1];
}
// ============================================================================
// HISTORISCHE DATEN ABRUFEN
// ============================================================================
async function pollHistoricalData() {
if (stopping || pollRunning) return;
pollRunning = true;
try {
const rawData = await getHistoricalData(
CONFIG.hhId,
CONFIG.midId
);
const records = normalizeHistoricalRecords(rawData);
debugLog("Historische Datensätze: " + records.length);
if (!records.length) {
throw new Error(
"OBI Historical API liefert keine Datensätze."
);
}
// VERBRAUCH
const energy = latestMeasurement(records, "energy");
if (energy) {
const value = Number(energy.value);
if (Number.isFinite(value)) {
await setStateAsync(stateId("energy_wh"), {
val: value,
ack: true
});
await setStateAsync(stateId("energy_kwh"), {
val: value / 1000,
ack: true
});
await setStateAsync(stateId("energy_time"), {
val: measurementTimestamp(energy),
ack: true
});
await setStateAsync(stateId("last_record_received"), {
val: measurementTimestamp(energy),
ack: true
});
}
}
// EINSPEISUNG
const feedIn = latestMeasurement(
records,
"negative_energy"
);
if (feedIn) {
const value = Number(feedIn.value);
if (Number.isFinite(value)) {
await setStateAsync(stateId("negative_energy_wh"), {
val: value,
ack: true
});
await setStateAsync(stateId("negative_energy_kwh"), {
val: value / 1000,
ack: true
});
await setStateAsync(stateId("negative_energy_time"), {
val: measurementTimestamp(feedIn),
ack: true
});
}
}
// AKTUALISIERUNG
await setStateAsync(stateId("last_update"), {
val: berlinTime(),
ack: true
});
await setStateAsync(stateId("last_error"), {
val: "",
ack: true
});
log(
"OBI Energy: Historische Daten aktualisiert.",
"info"
);
} catch (error) {
const message = getErrorMessage(error);
log(
"OBI Energy Poll Fehler: " + message,
"error"
);
await setStateAsync(stateId("last_error"), {
val: message,
ack: true
});
} finally {
pollRunning = false;
}
}
// ENDE TEIL 1
// ============================================================================
// LIVE MESSAGE PARSER
// ============================================================================
async function handleLiveJsonMessage(message) {
if (!message || typeof message !== "object") return false;
if (message.event && message.event !== "mqttMessage") {
return false;
}
let payload = message.data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch {
return false;
}
}
if (
payload &&
typeof payload === "object" &&
payload.payload !== undefined
) {
let nested = payload.payload;
if (typeof nested === "string") {
try {
nested = JSON.parse(nested);
} catch {
nested = null;
}
}
if (nested && typeof nested === "object") {
payload = nested;
}
}
if (!payload || typeof payload !== "object") return false;
const rawPower =
payload.power ??
payload.currentPower ??
payload.activePower ??
payload.powerW ??
payload.power_w ??
payload.value?.power ??
payload.values?.power;
const rawRssi =
payload.rssi ??
payload.signalStrength ??
payload.value?.rssi;
const rawBattery =
payload.battery ??
payload.batteryLevel ??
payload.value?.battery;
const power = Number(rawPower);
const rssi = Number(rawRssi);
const battery = Number(rawBattery);
lastLiveMessageAt = Date.now();
await setStateAsync(stateId("sensor_online"), {
val: true,
ack: true
});
const now = Date.now();
if (
now - lastLiveStateWriteAt <
CONFIG.liveWriteMinIntervalMs
) {
return true;
}
lastLiveStateWriteAt = now;
let validPower = false;
if (Number.isFinite(power)) {
await setStateAsync(stateId("power_w"), {
val: power,
ack: true
});
await setStateAsync(stateId("power_available"), {
val: true,
ack: true
});
validPower = true;
} else {
await setStateAsync(stateId("power_available"), {
val: false,
ack: true
});
}
if (Number.isFinite(rssi)) {
await setStateAsync(stateId("rssi"), {
val: rssi,
ack: true
});
}
if (Number.isFinite(battery)) {
await setStateAsync(stateId("battery"), {
val: battery,
ack: true
});
}
await setStateAsync(stateId("live_last_update"), {
val: berlinTime(),
ack: true
});
await setStateAsync(stateId("live_stale"), {
val: false,
ack: true
});
log(
"OBI LIVE: Power=" +
(validPower ? power + " W" : "nicht vorhanden") +
", Batterie=" +
(Number.isFinite(battery) ? battery + "%" : "unbekannt"),
"info"
);
// Sobald ein gültiger Live-Datensatz empfangen wurde,
// wird der Live-Zyklus beendet.
if (liveCycleActive && !liveMessageReceived) {
liveMessageReceived = true;
log(
"OBI LIVE: Datensatz empfangen. Live wird beendet.",
"info"
);
setTimeout(() => {
if (!stopping && liveCycleActive) {
finishLiveCycle();
}
}, 1000);
}
return true;
}
// ============================================================================
// JSON MESSAGE SPLITTER
// ============================================================================
function splitJsonMessages(text) {
const messages = [];
try {
messages.push(JSON.parse(text));
return messages;
} catch {}
let start = -1;
let depth = 0;
let inString = false;
let escaped = false;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (inString) {
if (escaped) {
escaped = false;
} else if (char === "\\") {
escaped = true;
} else if (char === '"') {
inString = false;
}
continue;
}
if (char === '"') {
inString = true;
continue;
}
if (char === "{") {
if (depth === 0) start = i;
depth++;
}
if (char === "}") {
depth--;
if (depth === 0 && start >= 0) {
try {
messages.push(
JSON.parse(text.substring(start, i + 1))
);
} catch {}
start = -1;
}
}
}
return messages;
}
// ============================================================================
// LIVE RAW MESSAGE
// ============================================================================
async function handleLiveRawMessage(raw) {
const text = Buffer.isBuffer(raw)
? raw.toString("utf8")
: String(raw);
const messages = splitJsonMessages(text);
for (const msg of messages) {
await handleLiveJsonMessage(msg);
}
}
// ============================================================================
// LIVE SOCKET SCHLIESSEN
// ============================================================================
function closeLiveSocket() {
if (liveSocket) {
const socket = liveSocket;
liveSocket = null;
try {
socket.close();
} catch {}
}
liveSocketConnecting = false;
}
// ============================================================================
// LIVE ZYKLUS BEENDEN
// ============================================================================
async function finishLiveCycle() {
if (stopping || !liveCycleActive) return;
// Doppelte Aufrufe verhindern
liveCycleActive = false;
if (liveAttemptTimer) {
clearTimeout(liveAttemptTimer);
liveAttemptTimer = null;
}
if (liveReconnectTimer) {
clearTimeout(liveReconnectTimer);
liveReconnectTimer = null;
}
log(
"OBI LIVE: Zurück in den Batteriesparmodus.",
"info"
);
// Verbindung schließen
closeLiveSocket();
// Sensor auf 300 Sekunden zurückstellen
try {
await setSensorUploadInterval(
CONFIG.midId,
CONFIG.normalUploadIntervalSeconds
);
await setStateAsync(stateId("live_connected"), {
val: false,
ack: true
});
await setStateAsync(stateId("live_cycle_active"), {
val: false,
ack: true
});
log(
"OBI Energy: Batteriesparmodus aktiv. Nächster Live-Zyklus in " +
CONFIG.liveCycleIntervalMinutes +
" Minuten.",
"info"
);
} catch (error) {
log(
"OBI LIVE: Fehler beim Zurückschalten: " +
getErrorMessage(error),
"error"
);
await setStateAsync(stateId("last_error"), {
val: getErrorMessage(error),
ack: true
});
}
}
// ============================================================================
// LIVE TIMEOUT
// ============================================================================
function startLiveAttemptTimeout() {
if (liveAttemptTimer) {
clearTimeout(liveAttemptTimer);
}
liveAttemptTimer = setTimeout(() => {
if (stopping || !liveCycleActive) return;
log(
"OBI LIVE: Zeitlimit erreicht. Live wird beendet.",
"warn"
);
finishLiveCycle();
}, CONFIG.liveAttemptTimeoutSeconds * 1000);
}
// ============================================================================
// LIVE VERBINDUNG
// ============================================================================
async function startLiveConnection() {
if (
stopping ||
!liveCycleActive ||
liveSocket ||
liveSocketConnecting
) {
return;
}
liveSocketConnecting = true;
try {
await ensureLoggedIn();
const url =
`${LIVE_DATA_URL}` +
`?bridgeId=${encodeURIComponent(CONFIG.hhId)}` +
`&sensorId=${encodeURIComponent(CONFIG.midId)}`;
const socket = new WebSocket(url, {
headers: {
Authorization: `Bearer ${token}`,
"User-Agent": LIVE_USER_AGENT,
"Accept-Language": ACCEPT_LANGUAGE
},
perMessageDeflate: true
});
liveSocket = socket;
socket.on("open", async () => {
liveSocketConnecting = false;
if (stopping || !liveCycleActive) {
socket.close();
return;
}
await setStateAsync(stateId("live_connected"), {
val: true,
ack: true
});
log(
"OBI Energy: Live WebSocket verbunden.",
"info"
);
});
socket.on("message", raw => {
handleLiveRawMessage(raw).catch(error => {
log(
"OBI Live Fehler: " + getErrorMessage(error),
"error"
);
});
});
socket.on("error", error => {
log(
"OBI WebSocket Fehler: " + getErrorMessage(error),
"warn"
);
});
socket.on("close", () => {
if (liveSocket === socket) {
liveSocket = null;
}
liveSocketConnecting = false;
if (
!stopping &&
liveCycleActive &&
!liveMessageReceived
) {
scheduleLiveReconnect();
}
});
} catch (error) {
liveSocket = null;
liveSocketConnecting = false;
throw error;
}
}
// ============================================================================
// LIVE RECONNECT
// ============================================================================
function scheduleLiveReconnect() {
if (
stopping ||
!liveCycleActive ||
liveMessageReceived ||
liveReconnectTimer
) {
return;
}
liveReconnectTimer = setTimeout(async () => {
liveReconnectTimer = null;
if (
stopping ||
!liveCycleActive ||
liveMessageReceived
) {
return;
}
try {
await startLiveConnection();
} catch (error) {
log(
"OBI Reconnect Fehler: " +
getErrorMessage(error),
"warn"
);
scheduleLiveReconnect();
}
}, CONFIG.liveReconnectDelayMs);
}
// ============================================================================
// LIVE ZYKLUS STARTEN
// ============================================================================
async function startLiveCycle() {
if (
stopping ||
!CONFIG.liveCycleEnabled ||
liveCycleActive
) {
return;
}
liveCycleActive = true;
liveMessageReceived = false;
log(
"OBI LIVE: Neuer Live-Zyklus wird gestartet.",
"info"
);
await setStateAsync(stateId("live_cycle_active"), {
val: true,
ack: true
});
try {
// Sensor auf 2 Sekunden setzen
await setSensorUploadInterval(
CONFIG.midId,
CONFIG.liveUploadIntervalSeconds
);
// WebSocket verbinden
await startLiveConnection();
// Maximale Live-Dauer überwachen
startLiveAttemptTimeout();
} catch (error) {
log(
"OBI LIVE Startfehler: " +
getErrorMessage(error),
"error"
);
await finishLiveCycle();
}
}
// ============================================================================
// LIVE ZYKLUS PLANEN
// ============================================================================
function scheduleNextLiveCycle() {
if (stopping || !CONFIG.liveCycleEnabled) return;
if (liveCycleTimer) {
clearTimeout(liveCycleTimer);
liveCycleTimer = null;
}
liveCycleTimer = setTimeout(async () => {
liveCycleTimer = null;
if (stopping) return;
await startLiveCycle();
}, CONFIG.liveCycleIntervalMinutes * 60 * 1000);
}
// ============================================================================
// LIVE WATCHDOG
// ============================================================================
function startLiveWatchdog() {
if (liveWatchdogTimer) {
clearInterval(liveWatchdogTimer);
}
liveWatchdogTimer = setInterval(async () => {
if (stopping) return;
const stale =
!lastLiveMessageAt ||
Date.now() - lastLiveMessageAt >
CONFIG.liveStaleAfterMs;
await setStateAsync(stateId("live_stale"), {
val: stale,
ack: true
});
}, 15000);
}
// ============================================================================
// DATENPUNKTE ERSTELLEN
// ============================================================================
async function createStateIfMissing(name, value, common) {
const id = stateId(name);
if (!existsState(id)) {
await createStateAsync(
id,
value,
{
...common,
read: true,
write: false
}
);
}
}
// ============================================================================
// ALLE DATENPUNKTE
// ============================================================================
async function ensureStates() {
const states = {
power_w: [0, {
name: "Live Leistung",
type: "number",
unit: "W"
}],
power_available: [false, {
name: "Leistung verfügbar",
type: "boolean"
}],
energy_wh: [0, {
name: "Verbrauch",
type: "number",
unit: "Wh"
}],
energy_kwh: [0, {
name: "Verbrauch",
type: "number",
unit: "kWh"
}],
negative_energy_wh: [0, {
name: "Einspeisung",
type: "number",
unit: "Wh"
}],
negative_energy_kwh: [0, {
name: "Einspeisung",
type: "number",
unit: "kWh"
}],
rssi: [0, {
name: "RSSI",
type: "number",
unit: "dBm"
}],
battery: [0, {
name: "Batterie",
type: "number",
unit: "%"
}],
sensor_online: [false, {
name: "Sensor Online",
type: "boolean"
}],
connection_strength: ["", {
name: "Verbindungsqualität",
type: "string"
}],
last_record_received: ["", {
name: "Letzter Sensorwert",
type: "string"
}],
live_last_update: ["", {
name: "Letztes Live Signal",
type: "string"
}],
live_connected: [false, {
name: "Live Verbindung",
type: "boolean"
}],
live_stale: [true, {
name: "Live Daten veraltet",
type: "boolean"
}],
live_upload_interval: [300, {
name: "Upload Intervall",
type: "number",
unit: "s"
}],
live_cycle_active: [false, {
name: "Live Zyklus aktiv",
type: "boolean"
}],
bridge_id: [CONFIG.hhId, {
name: "Bridge ID",
type: "string"
}],
sensor_id: [CONFIG.midId, {
name: "Sensor ID",
type: "string"
}],
energy_time: ["", {
name: "Zeit Verbrauch",
type: "string"
}],
negative_energy_time: ["", {
name: "Zeit Einspeisung",
type: "string"
}],
last_update: ["", {
name: "Letzte Aktualisierung",
type: "string"
}],
last_error: ["", {
name: "Letzter Fehler",
type: "string"
}]
};
for (const key in states) {
await createStateIfMissing(
key,
states[key][0],
states[key][1]
);
}
}
// ============================================================================
// START
// ============================================================================
async function start() {
validateConfiguration();
await ensureStates();
await setStateAsync(stateId("bridge_id"), {
val: CONFIG.hhId,
ack: true
});
await setStateAsync(stateId("sensor_id"), {
val: CONFIG.midId,
ack: true
});
// Zuerst Batteriesparmodus aktivieren
await setSensorUploadInterval(
CONFIG.midId,
CONFIG.normalUploadIntervalSeconds
);
await setStateAsync(stateId("live_cycle_active"), {
val: false,
ack: true
});
await setStateAsync(stateId("live_connected"), {
val: false,
ack: true
});
await setStateAsync(stateId("live_stale"), {
val: true,
ack: true
});
// Historische Daten laden
await pollHistoricalData();
// Watchdog starten
startLiveWatchdog();
// Ersten Live-Zyklus starten
if (CONFIG.liveCycleEnabled) {
await startLiveCycle();
} else {
log(
"OBI Energy: Zyklischer Live-Betrieb deaktiviert.",
"info"
);
}
log(
"OBI Energy: Skript erfolgreich gestartet.",
"info"
);
}
// ============================================================================
// START AUSFÜHREN
// ============================================================================
start().catch(error => {
log(
"OBI Start Fehler: " + getErrorMessage(error),
"error"
);
setStateAsync(stateId("last_error"), {
val: getErrorMessage(error),
ack: true
}).catch(() => {});
});
// ============================================================================
// HISTORISCHE DATEN ZEITPLAN
// ============================================================================
schedule(CONFIG.pollCron, () => {
if (!stopping) {
pollHistoricalData();
}
});
// ============================================================================
// LIVE ZYKLUS WIEDERHOLEN
// ============================================================================
// Nach jedem abgeschlossenen Live-Zyklus wird der nächste
// Live-Zyklus für 5 Minuten später vorgemerkt.
const originalFinishLiveCycle = finishLiveCycle;
finishLiveCycle = async function() {
await originalFinishLiveCycle();
if (!stopping && CONFIG.liveCycleEnabled) {
scheduleNextLiveCycle();
}
};
// ============================================================================
// SKRIPT BEENDEN
// ============================================================================
onStop(async () => {
stopping = true;
log(
"OBI Energy: Skript wird beendet.",
"info"
);
// Alle Timer stoppen
if (liveCycleTimer) {
clearTimeout(liveCycleTimer);
liveCycleTimer = null;
}
if (liveAttemptTimer) {
clearTimeout(liveAttemptTimer);
liveAttemptTimer = null;
}
if (liveReconnectTimer) {
clearTimeout(liveReconnectTimer);
liveReconnectTimer = null;
}
if (liveWatchdogTimer) {
clearInterval(liveWatchdogTimer);
liveWatchdogTimer = null;
}
// Live-Zustand beenden
liveCycleActive = false;
// WebSocket schließen
closeLiveSocket();
// Sensor auf 300 Sekunden zurücksetzen.
// Nur wenn noch ein gültiger Token vorhanden ist.
if (token && !tokenIsStale()) {
try {
const response = await http.patch(
`${SENSOR_URL}/${encodeURIComponent(CONFIG.midId)}`,
{
id: CONFIG.midId,
uploadInterval:
CONFIG.normalUploadIntervalSeconds
},
{
headers: {
Authorization: `Bearer ${token}`,
Accept: ACCEPT_SENSOR,
"Content-Type": ACCEPT_SENSOR,
"User-Agent": LIVE_USER_AGENT,
"X-Platform": "iOS",
"X-Lib-Version": "26.6.9"
}
}
);
if (
response.status >= 200 &&
response.status < 300
) {
await setStateAsync(
stateId("live_upload_interval"),
{
val: CONFIG.normalUploadIntervalSeconds,
ack: true
}
);
log(
"OBI Energy: Sensorintervall beim Beenden auf 300 Sekunden zurückgesetzt.",
"info"
);
} else {
log(
"OBI Restore Fehler: HTTP " +
response.status,
"warn"
);
}
} catch (error) {
log(
"OBI Restore Fehler: " +
getErrorMessage(error),
"warn"
);
}
} else {
log(
"OBI Restore nicht möglich: Kein gültiger Token.",
"warn"
);
}
await setStateAsync(stateId("live_connected"), {
val: false,
ack: true
});
await setStateAsync(stateId("live_cycle_active"), {
val: false,
ack: true
});
// Zugangsdaten im Speicher freigeben
token = null;
tokenObtainedAt = 0;
loginPromise = null;
}, 15000);