nomos system nomos system

Geräteprofil · Verschiedenes

Dynamische Strompreise

Dieses Profil bringt den Börsenstrompreis nach nomos: als Kachel in der App und als Wert, mit dem Automationen rechnen können — Waschmaschine, Wärmepumpe oder Wallbox laufen dann, wenn der Strom günstig ist.

Das Profil holt die Preise nicht selbst. Es ist die nomos-Hälfte der Verbindung; die andere Hälfte ist ein Node-RED-Flow, der die Preise abruft und auf MQTT veröffentlicht. Zwei fertige Flows stehen weiter unten zum Einfügen bereit.

Voraussetzungen

  1. Ein MQTT-Broker, in nomos unter Skills → MQTT eingetragen.
  2. Node-RED mit Zugriff auf denselben Broker. Die Flows brauchen nur Standardknoten — es ist nichts nachzuinstallieren.
  3. Eine Preisquelle:

    Quelle Konto nötig Was sie liefert
    aWATTar (Deutschland, Österreich) nein den reinen Börsenpreis je Stunde
    Tibber ja, mit persönlichem Token den Endpreis inklusive Steuern und Abgaben

Einrichtung in nomos

  1. Skills → Add-ons: das Profil importieren
  2. Gerät anlegen und ein MQTT-Topic vergeben, zum Beispiel strompreis

Mehr ist in nomos nicht zu tun. Das Gerät bleibt leer, bis der Flow läuft.

Den Flow einfügen

In Node-RED: Menü rechts oben (☰) → Import → den JSON-Text einfügen → Import → Deploy.

Danach sind zwei Dinge anzupassen:

Flow für aWATTar

JSON zum Einfügen
[{"id":"nomos_price_awattar","type":"tab","label":"Strompreis (aWATTar)","disabled":false,"info":""},{"id":"nomos_price_awattar_broker","type":"mqtt-broker","name":"nomos Broker","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"tele/strompreis/LWT","birthQos":"0","birthRetain":"true","birthPayload":"Online","birthMsg":{},"closeTopic":"tele/strompreis/LWT","closeQos":"0","closeRetain":"true","closePayload":"Offline","closeMsg":{},"willTopic":"tele/strompreis/LWT","willQos":"0","willRetain":"true","willPayload":"Offline","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"nomos_price_awattar_tick","type":"inject","z":"nomos_price_awattar","name":"alle 15 Minuten","props":[{"p":"payload"}],"repeat":"900","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["nomos_price_awattar_range"]]},{"id":"nomos_price_awattar_range","type":"function","z":"nomos_price_awattar","name":"Zeitraum","func":"// Zeitraum: ab heute 00:00 fuer 48 Stunden. Ohne Zeitraum liefert die Schnittstelle\n// nur die restlichen Stunden - dann waeren Tagesbestwert und Tagesmittel falsch.\nconst midnight = new Date();\nmidnight.setHours(0, 0, 0, 0);\nmsg.payload = {\n    start: midnight.getTime(),\n    end: midnight.getTime() + 48 * 3600 * 1000\n};\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":100,"wires":[["nomos_price_awattar_req"]]},{"id":"nomos_price_awattar_req","type":"http request","z":"nomos_price_awattar","name":"aWATTar","method":"GET","ret":"obj","paytoqs":"query","url":"https://api.awattar.de/v1/marketdata","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":560,"y":100,"wires":[["nomos_price_awattar_fn"]]},{"id":"nomos_price_awattar_fn","type":"function","z":"nomos_price_awattar","name":"Preise aufbereiten","func":"// aWATTar liefert Marktpreise in Eur/MWh je Stunde; nomos bekommt ct/kWh.\n// TOPIC muss mit dem MQTT-Topic des Geraets in nomos uebereinstimmen.\nconst TOPIC = \"strompreis\";\n\nconst data = (msg.payload && msg.payload.data) || [];\nif (!data.length) { node.warn(\"keine Preisdaten\"); return null; }\n\nconst ct = (eurPerMWh) => Math.round(eurPerMWh / 10 * 100) / 100;\nconst now = Date.now();\nconst midnight = new Date(); midnight.setHours(0, 0, 0, 0);\nconst dayStart = midnight.getTime();\nconst dayEnd = dayStart + 24 * 3600 * 1000;\n\nconst today = data.filter((e) => e.start_timestamp >= dayStart && e.start_timestamp < dayEnd);\nconst current = data.find((e) => now >= e.start_timestamp && now < e.end_timestamp);\nif (!current || !today.length) { node.warn(\"keine Preise fuer jetzt\"); return null; }\nconst next = data.find((e) => e.start_timestamp >= current.end_timestamp);\n\nconst prices = today.map((e) => e.marketprice);\nconst min = Math.min.apply(null, prices);\nconst max = Math.max.apply(null, prices);\nconst avg = prices.reduce((a, b) => a + b, 0) / prices.length;\nconst cheapest = today.find((e) => e.marketprice === min);\n\n// Eine Einstufung, die der von Tibber entspricht, damit beide Flows dasselbe liefern.\nlet level = \"NORMAL\";\nconst p = current.marketprice;\nif (max - min < 1) level = \"NORMAL\";          // ein flacher Tag ist weder billig noch teuer\nelse if (p <= min + (avg - min) * 0.3) level = \"VERY_CHEAP\";\nelse if (p < avg) level = \"CHEAP\";\nelse if (p >= max - (max - avg) * 0.3) level = \"VERY_EXPENSIVE\";\nelse level = \"EXPENSIVE\";\n\nconst hhmm = (t) => new Date(t).toLocaleTimeString(\"de-DE\", { hour: \"2-digit\", minute: \"2-digit\" });\n\nconst values = [\n    [\"PRICE\", ct(p)],\n    [\"ENERGY\", ct(p)],\n    [\"NEXT\", next ? ct(next.marketprice) : ct(p)],\n    [\"MIN\", ct(min)],\n    [\"MAX\", ct(max)],\n    [\"AVG\", Math.round(ct(avg) * 100) / 100],\n    [\"CHEAPEST\", hhmm(cheapest.start_timestamp)],\n    [\"IS_CHEAP\", p < avg ? 1 : 0],\n    [\"LEVEL\", level],\n    [\"UPDATED\", hhmm(now)]\n];\n\nreturn [values.map(([key, value]) => ({ topic: \"stat/\" + TOPIC + \"/\" + key, payload: value, retain: true }))];\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":760,"y":100,"wires":[["nomos_price_awattar_out"]]},{"id":"nomos_price_awattar_out","type":"mqtt out","z":"nomos_price_awattar","name":"an nomos","topic":"","qos":"0","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"nomos_price_awattar_broker","x":980,"y":100,"wires":[]}]

Für Österreich im Knoten http request die Adresse auf https://api.awattar.at/v1/marketdata ändern — es ist ein eigener Markt mit eigenen Preisen.

Der Flow fragt alle 15 Minuten den Zeitraum ab heute 00:00 für 48 Stunden ab. Das ist Absicht: ohne Zeitraum liefert die Schnittstelle nur die noch kommenden Stunden, und Tagesmittel wie günstigste Stunde wären dann nur die des Tagesrests.

Flow für Tibber

JSON zum Einfügen
[{"id":"nomos_price_tibber","type":"tab","label":"Strompreis (Tibber)","disabled":false,"info":""},{"id":"nomos_price_tibber_broker","type":"mqtt-broker","name":"nomos Broker","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"tele/strompreis/LWT","birthQos":"0","birthRetain":"true","birthPayload":"Online","birthMsg":{},"closeTopic":"tele/strompreis/LWT","closeQos":"0","closeRetain":"true","closePayload":"Offline","closeMsg":{},"willTopic":"tele/strompreis/LWT","willQos":"0","willRetain":"true","willPayload":"Offline","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"nomos_price_tibber_tick","type":"inject","z":"nomos_price_tibber","name":"alle 15 Minuten","props":[{"p":"payload"}],"repeat":"900","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["nomos_price_tibber_q"]]},{"id":"nomos_price_tibber_q","type":"change","z":"nomos_price_tibber","name":"Abfrage","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"query\":\"{viewer{homes{currentSubscription{priceInfo{current{total energy tax startsAt level} today{total energy tax startsAt level}}}}}}\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":100,"wires":[["nomos_price_tibber_req"]]},{"id":"nomos_price_tibber_req","type":"http request","z":"nomos_price_tibber","name":"Tibber","method":"POST","ret":"obj","paytoqs":"ignore","url":"https://api.tibber.com/v1-beta/gql","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"other","keyValue":"Authorization","valueType":"other","valueValue":"Bearer IHR-TIBBER-TOKEN"},{"keyType":"other","keyValue":"Content-Type","valueType":"other","valueValue":"application/json"}],"x":380,"y":100,"wires":[["nomos_price_tibber_fn"]]},{"id":"nomos_price_tibber_fn","type":"function","z":"nomos_price_tibber","name":"Preise aufbereiten","func":"// Tibber liefert Preise in EUR/kWh inklusive Steuern und Abgaben; nomos bekommt ct/kWh.\n// TOPIC muss mit dem MQTT-Topic des Geraets in nomos uebereinstimmen.\nconst TOPIC = \"strompreis\";\n\nconst home = msg.payload && msg.payload.data && msg.payload.data.viewer\n    && msg.payload.data.viewer.homes && msg.payload.data.viewer.homes[0];\nconst info = home && home.currentSubscription && home.currentSubscription.priceInfo;\nif (!info || !info.current || !info.today || !info.today.length) {\n    node.warn(\"keine Preisdaten von Tibber\"); return null;\n}\n\nconst ct = (eurPerKWh) => Math.round(eurPerKWh * 100 * 100) / 100;\nconst now = Date.now();\nconst cur = info.current;\nconst today = info.today;\nconst prices = today.map((e) => e.total);\nconst min = Math.min.apply(null, prices);\nconst max = Math.max.apply(null, prices);\nconst avg = prices.reduce((a, b) => a + b, 0) / prices.length;\nconst cheapest = today.find((e) => e.total === min);\nconst next = today.find((e) => new Date(e.startsAt).getTime() > new Date(cur.startsAt).getTime());\n\nconst hhmm = (iso) => new Date(iso).toLocaleTimeString(\"de-DE\", { hour: \"2-digit\", minute: \"2-digit\" });\n\nconst values = [\n    [\"PRICE\", ct(cur.total)],\n    [\"ENERGY\", ct(cur.energy)],\n    [\"NEXT\", next ? ct(next.total) : ct(cur.total)],\n    [\"MIN\", ct(min)],\n    [\"MAX\", ct(max)],\n    [\"AVG\", Math.round(ct(avg) * 100) / 100],\n    [\"CHEAPEST\", hhmm(cheapest.startsAt)],\n    [\"IS_CHEAP\", cur.total < avg ? 1 : 0],\n    [\"LEVEL\", cur.level || \"NORMAL\"],\n    [\"UPDATED\", hhmm(new Date(now).toISOString())]\n];\n\nreturn [values.map(([key, value]) => ({ topic: \"stat/\" + TOPIC + \"/\" + key, payload: value, retain: true }))];\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":100,"wires":[["nomos_price_tibber_out"]]},{"id":"nomos_price_tibber_out","type":"mqtt out","z":"nomos_price_tibber","name":"an nomos","topic":"","qos":"0","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"nomos_price_tibber_broker","x":830,"y":100,"wires":[]}]

Im Knoten http request steht im Kopf Authorization der Platzhalter Bearer IHR-TIBBER-TOKEN. Nur den Platzhalter ersetzen — das Wort Bearer und das Leerzeichen danach bleiben stehen. Das Token wird im Konto des Anbieters erzeugt.

Was der Flow veröffentlicht

<Topic> ist das MQTT-Topic des Geräts in nomos. Alle Werte werden retained veröffentlicht, damit nach einem Neustart sofort wieder ein Preis dasteht.

Topic Inhalt
stat/<Topic>/PRICE Preis der laufenden Stunde in ct/kWh
stat/<Topic>/LEVEL Einstufung: VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE
stat/<Topic>/IS_CHEAP 1, solange der Preis unter dem Tagesmittel liegt, sonst 0
stat/<Topic>/MIN günstigster Preis des Tages
stat/<Topic>/MAX teuerster Preis des Tages
stat/<Topic>/AVG Tagesmittel
stat/<Topic>/CHEAPEST Beginn der günstigsten Stunde, z. B. 03:00
stat/<Topic>/NEXT Preis der nächsten Stunde
stat/<Topic>/ENERGY Energieanteil des Preises (bei aWATTar derselbe Wert wie PRICE)
stat/<Topic>/UPDATED Uhrzeit der letzten Abfrage
tele/<Topic>/LWT Online bzw. Offline — das Lebenszeichen, das der Broker selbst setzt

Datenbereich

In Automationen verwenden

Hinweise