Integrationen
ioBroker einrichten
Ein lokales JavaScript sendet Heartbeats und erkennt Batterie-Datenpunkte automatisch.
Voraussetzungen
- ioBroker mit JavaScript-Adapter ab Version 7.9.0
- Werklio-Standort mit ioBroker-Integration
- Standort-ID und API-Key
Schritt für Schritt
- Erzeuge die ioBroker-Integration in Werklio. Direkt danach wird der vollständige Connector mit eingesetzter Standort-ID und API-Key einmalig angezeigt.
- Erstelle im JavaScript-Adapter ein neues Skript und füge den gesamten Connector-Code ein.
- Das Skript findet States mit den Rollen value.battery und indicator.lowbat automatisch; gängige Battery-/LOWBAT-IDs werden nur bei passendem Datentyp als Fallback berücksichtigt.
- Die Standardkonfiguration überwacht zusätzlich Admin-Verbindung, Update-Status, letzte Update-Prüfung und nur die Anzahl aktiver Socket-Clients.
- Ergänze bei Bedarf bewusst ausgewählte Datenpunkte in additionalStates.
- Starte das Skript und prüfe die Logeinträge ohne Ausgabe des Keys.
- Kontrolliere Heartbeat, Batterieübersicht und Geräte unter Werklio.
Beispielkonfiguration
// Werklio Connector für ioBroker JavaScript-Adapter
// Benötigt ioBroker JavaScript-Adapter >= 7.9.0 (native Funktion httpPost).
const WERKLIO_URL = 'https://app.werkl.io';
const SITE_ID = 'DEINE-STANDORT-GUID';
const API_KEY = 'wk_live_DEIN_EINMALIGER_KEY';
const CONNECTOR_VERSION = '2.0.0';
const CONNECTOR_STARTED_AT = Date.now();
const isTrue = value => value === true || value === 'true';
const connectedClientCount = value => {
if (Array.isArray(value)) return value.length;
const match = String(value || '').match(/^\[(\d+)\]/);
return match ? Number(match[1]) : 0;
};
const standardStates = [
{
id: 'admin.0.info.connection',
name: 'ioBroker Admin HTTP-Server',
type: 'service',
critical: true,
status: value => isTrue(value) ? 'online' : 'offline',
diagnostic: value => isTrue(value) ? 'HTTP-Server gestartet' : 'HTTP-Server nicht gestartet'
},
{
id: 'admin.0.info.newUpdates',
name: 'ioBroker Adapter-Updates',
type: 'service',
critical: false,
status: value => isTrue(value) ? 'warning' : 'online',
diagnostic: value => isTrue(value) ? 'Neue Adapter-Updates verfügbar' : 'Keine neuen Adapter-Updates gemeldet'
},
{
id: 'admin.0.info.updatesNumber',
name: 'Verfügbare ioBroker Updates',
type: 'service',
critical: false,
status: value => Number(value || 0) > 0 ? 'warning' : 'online',
diagnostic: value => `${Number(value || 0)} verfügbare Updates`
},
{
id: 'admin.0.info.lastUpdateCheck',
name: 'Letzte ioBroker Update-Prüfung',
type: 'sensor',
critical: false,
status: value => value ? 'online' : 'unknown',
diagnostic: value => value ? `Letzte Prüfung: ${String(value)}` : 'Noch keine Update-Prüfung gemeldet'
},
{
id: 'admin.0.info.connected',
name: 'ioBroker Admin-Verbindungen',
type: 'service',
critical: false,
status: value => value !== null && value !== undefined ? 'online' : 'unknown',
diagnostic: value => `Aktive Socket-Clients: ${connectedClientCount(value)}`
}
];
// Hier nur bewusst ausgewählte Healthcheck-Datenpunkte ergänzen.
const additionalStates = [
// Beispiel: Temperaturwert unter Geräte & Entitäten anzeigen.
// {
// id: 'sainlogic.0.weather.current.outdoortemp',
// name: 'Außentemperatur',
// type: 'sensor',
// critical: false,
// value: value => Number(value),
// unit: '°C',
// status: value => value !== null && value !== undefined && Number.isFinite(Number(value)) ? 'online' : 'unknown',
// diagnostic: value => value !== null && value !== undefined && Number.isFinite(Number(value))
// ? `${Number(value).toFixed(1)} °C`
// : 'Kein gültiger Temperaturwert'
// },
// Beispiel: Erreichbarkeit eines Sensors überwachen.
// {
// id: 'zigbee.0.sensor_bedroom.available',
// name: 'Zigbee Sensor Schlafzimmer',
// type: 'sensor',
// critical: false,
// status: value => isTrue(value) ? 'online' : 'offline',
// diagnostic: value => isTrue(value) ? 'Erreichbar' : 'Nicht erreichbar'
// }
];
function sourceVersion() {
const adminObject = getObject('system.adapter.admin.0');
const adminVersion = adminObject && adminObject.common && adminObject.common.version;
return adminVersion ? `admin-${adminVersion} / connector-${CONNECTOR_VERSION}` : `connector-${CONNECTOR_VERSION}`;
}
function stateTimestamp(state) {
const timestamp = state && Number(state.ts);
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
}
function localizedName(value) {
if (typeof value === 'string' && value.trim()) return value.trim();
if (value && typeof value === 'object') return value.de || value.en || Object.values(value).find(entry => typeof entry === 'string') || null;
return null;
}
function batteryItemName(id, value) {
const baseName = localizedName(value) || 'Batterie';
if (!/^(?:battery|battery capacity|battery level|battery percentage|batterie|batteriekapazität|batteriestand|akkustand)$/i.test(baseName)) return baseName;
const shelly = /^shelly\.\d+\.([^.#]+)(?:#([^.#]+))?/i.exec(id);
if (shelly) {
const model = shelly[1].replace(/-\d+$/, '');
return [baseName, 'Shelly', model, shelly[2]].filter(Boolean).join(' ');
}
const parts = id.split('.');
return parts.length >= 3 ? `${baseName} ${parts[0]} ${parts[2].replace(/[#_]/g, ' ')}` : baseName;
}
function discoveredBatteryIds() {
const ids = {};
const add = id => { if (id && Object.keys(ids).length < 1000) ids[id] = true; };
const safelySelect = (selector, callback) => {
try { $(selector).each(callback); }
catch (error) { log(`Werklio Batterie-Suche: Selektor ${selector} übersprungen (${error.message || error})`, 'warn'); }
};
['state[role=value.battery]', 'state[role=indicator.lowbat]'].forEach(selector => safelySelect(selector, add));
['state[id=*.battery]', 'state[id=*.batteryLevel]', 'state[id=*.LOWBAT]', 'state[id=*.lowbat]'].forEach(selector =>
safelySelect(selector, id => {
const object = getObject(id);
const common = object && object.common ? object.common : {};
if (common.unit === '%' || common.type === 'boolean') add(id);
}));
return Object.keys(ids);
}
function createBatteryItem(id) {
const state = getState(id);
const object = getObject(id);
const common = object && object.common ? object.common : {};
const role = String(common.role || '').toLowerCase();
const name = batteryItemName(id, common.name);
const lowBatteryFlag = role === 'indicator.lowbat' || (common.type === 'boolean' && /(?:lowbat|battery[_-]?low)/i.test(id));
const numeric = state ? Number(state.val) : NaN;
const validPercent = !lowBatteryFlag && Number.isFinite(numeric) && numeric >= 0 && numeric <= 100;
const validFlag = lowBatteryFlag && state && (state.val === true || state.val === false || state.val === 'true' || state.val === 'false' || state.val === 1 || state.val === 0);
const batteryLow = validFlag ? isTrue(state.val) || state.val === 1 : null;
return {
externalId: id,
name: name,
type: 'battery',
status: validPercent || validFlag ? 'online' : 'unknown',
value: validPercent ? Math.round(numeric) : batteryLow,
unit: validPercent ? '%' : null,
batteryPercent: validPercent ? Math.round(numeric) : null,
batteryLow: batteryLow,
lastSeenUtc: stateTimestamp(state),
diagnostic: validPercent ? `Gemeldeter Batteriestand: ${Math.round(numeric)}%` : validFlag ? (batteryLow ? 'Batterie schwach gemeldet' : 'Batterie in Ordnung gemeldet') : 'Kein gültiger Batteriewert',
isCritical: false
};
}
function createItem(definition) {
const state = getState(definition.id);
if (!state) {
return {
externalId: definition.id,
name: definition.name,
type: definition.type,
status: 'unknown',
value: null,
unit: definition.unit || null,
lastSeenUtc: null,
diagnostic: 'Datenpunkt in ioBroker nicht gefunden',
isCritical: definition.critical
};
}
return {
externalId: definition.id,
name: definition.name,
type: definition.type,
status: definition.status(state.val),
value: definition.value ? definition.value(state.val) : state.val,
unit: definition.unit || null,
lastSeenUtc: stateTimestamp(state),
diagnostic: definition.diagnostic(state.val),
isCritical: definition.critical
};
}
async function post(path, body) {
return new Promise((resolve, reject) => {
httpPost(
`${WERKLIO_URL}/api/ingest/iobroker/${path}`,
body,
{
timeout: 15000,
headers: {
'Content-Type': 'application/json',
'X-Werklio-Key': API_KEY,
'Idempotency-Key': `iob-${path}-${Date.now()}`
}
},
(error, response) => {
if (error) {
reject(new Error(`Werklio ${path}: ${error.message || error}`));
return;
}
if (!response || response.statusCode < 200 || response.statusCode >= 300) {
reject(new Error(`Werklio ${path}: HTTP ${response ? response.statusCode : 'ohne Antwort'}`));
return;
}
try {
const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
resolve(data || { status: 'accepted' });
} catch {
resolve({ status: 'accepted' });
}
}
);
});
}
async function heartbeat() {
return post('heartbeat', {
apiVersion: '2026-08-01',
siteId: SITE_ID,
source: 'iobroker',
sourceVersion: sourceVersion(),
sentAtUtc: new Date().toISOString(),
status: 'online',
uptimeSeconds: Math.floor((Date.now() - CONNECTOR_STARTED_AT) / 1000)
});
}
async function snapshot() {
const batteries = discoveredBatteryIds().map(createBatteryItem);
const items = [...standardStates, ...additionalStates].map(createItem).concat(batteries);
return post('snapshot', {
apiVersion: '2026-08-01',
siteId: SITE_ID,
source: 'iobroker',
sourceVersion: sourceVersion(),
sentAtUtc: new Date().toISOString(),
eventId: `iob-snapshot-${Date.now()}`,
isFullSnapshot: true,
items
});
}
async function run(label, action) {
try {
const result = await action();
log(`Werklio ${label}: ${result && result.status ? result.status : 'accepted'}`, 'info');
} catch (error) {
log(`Werklio ${label} fehlgeschlagen: ${error.message}`, 'error');
}
}
schedule('*/1 * * * *', () => run('Heartbeat', heartbeat));
schedule('*/5 * * * *', () => run('Snapshot', snapshot));
run('Heartbeat', heartbeat);
run('Snapshot', snapshot);
Testschritt
Skript manuell starten und im ioBroker-Log ausschließlich HTTP-Status/Fehlercode kontrollieren.
Erwartetes Ergebnis
Heartbeat erscheint binnen einer Minute; Batterie-Datenpunkte erscheinen ohne manuelle Liste im Batterie-Tab.
Fehlerbehebung
- httpPost fehlt: JavaScript-Adapter auf mindestens Version 7.9.0 aktualisieren.
- 403/401: API-Key und Integration prüfen; bei einem offengelegten Key zuerst rotieren.
- Falscher Status: Mapping im items-Block an die Bedeutung des Datenpunkts anpassen.
Sicherheitshinweise
- Das Skript und den darin enthaltenen API-Key nur für Administratoren zugänglich machen.
- newsFeed, updatesList und Namen verbundener Admin-Clients werden aus Gründen der Datenminimierung nicht übertragen.
- Keine kompletten Objektbäume oder personenbezogenen Werte übertragen.
- Skriptlogs dürfen Header und Body mit Key nicht ausgeben.