Werklio
☰

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

  1. Erzeuge die ioBroker-Integration in Werklio. Direkt danach wird der vollständige Connector mit eingesetzter Standort-ID und API-Key einmalig angezeigt.
  2. Erstelle im JavaScript-Adapter ein neues Skript und füge den gesamten Connector-Code ein.
  3. 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.
  4. Die Standardkonfiguration überwacht zusätzlich Admin-Verbindung, Update-Status, letzte Update-Prüfung und nur die Anzahl aktiver Socket-Clients.
  5. Ergänze bei Bedarf bewusst ausgewählte Datenpunkte in additionalStates.
  6. Starte das Skript und prüfe die Logeinträge ohne Ausgabe des Keys.
  7. 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.