Handige tools

Praktische plugins en hulpmiddelen voor iedereen met zonnepanelen of een slimme meter. Gratis te gebruiken, geen account vereist.

☀️ Domoticz plugin

Zonnepanelen­voorspelling via forecast.solar

Haalt elk uur de verwachte opbrengst op via forecast.solar en toont vandaag/morgen/huidig uur als virtuele Domoticz-sensoren.

☀️
Verwachte opbrengst vandaag (kWh) Bijgewerkt elk uur op basis van bewolkingsverwachting.
📅
Verwachte opbrengst morgen (kWh) Planeer je verbruik op de beste dag.
Verwacht huidig uur (W) Handig voor automatisering op basis van actuele productie.

Installatie

  1. Kopieer de map ForecastSolar/ naar <domoticz>/plugins/
  2. Herstart Domoticz
  3. Setup → Hardware → toevoegen → "Deel je energie — Zonnepanelen Voorspelling"
  4. Vul breedtegraad, lengtegraad, hellingshoek, oriëntatie en capaciteit in
  5. Activeer — de drie sensoren worden automatisch aangemaakt

Configuratieparameters

Parameter Voorbeeld Toelichting
Breedtegraad (lat) 52.3676 Geografische breedte van jouw locatie
Lengtegraad (lon) 4.9041 Geografische lengte van jouw locatie
Hellingshoek (°) 35 0 = plat, 90 = verticaal — typisch 30–40°
Oriëntatie (°) 180 Noord=0, Oost=90, Zuid=180, West=270
Capaciteit (kWp) 5.0 Totaal geïnstalleerd vermogen in kilowattpiek

Breedtegraad en lengtegraad vind je via latlong.net of Google Maps (rechtsklik op je adres).

ForecastSolar/plugin.py
"""
Deel je energie — forecast.solar Domoticz Plugin
=================================================
Leest elk uur de verwachte zonnepanelenopbrengst via forecast.solar
en werkt drie virtuele Domoticz-sensoren bij:

  1. Verwachte opbrengst vandaag  (kWh)
  2. Verwachte opbrengst morgen   (kWh)
  3. Verwacht huidig uur          (W)

Installatie:
  1. Kopieer deze map naar <domoticz>/plugins/ForecastSolar/
  2. Herstart Domoticz
  3. Ga naar Setup → Hardware → voeg toe: "Deel je energie — Zonnepanelen Voorspelling"
  4. Vul breedtegraad, lengtegraad, hellingshoek, oriëntatie en capaciteit in
  5. Activeer — de drie virtuele devices worden automatisch aangemaakt

Meer info: https://deeljeenergie.nl/handige-tools
"""

import json
import urllib.request
import urllib.error
from datetime import datetime, timedelta

"""
<plugin key="ForecastSolar" name="Deel je energie — Zonnepanelen Voorspelling" author="deeljeenergie.nl" version="1.0.0">
    <description>
        Leest elk uur de verwachte opbrengst van je zonnepanelen via forecast.solar
        en toont vandaag, morgen en het huidige uur als virtuele Domoticz-sensoren.
        Meer info: https://deeljeenergie.nl/handige-tools
    </description>
    <params>
        <param field="Address" label="Breedtegraad (lat)"  width="150px" required="true"  default="52.3676"/>
        <param field="Port"    label="Lengtegraad (lon)"   width="150px" required="true"  default="4.9041"/>
        <param field="Mode1"   label="Hellingshoek in graden (0=plat, 90=verticaal)" width="120px" required="true" default="35"/>
        <param field="Mode2"   label="Oriëntatie — Noord=0, Oost=90, Zuid=180, West=270" width="180px" required="true" default="180"/>
        <param field="Mode3"   label="Capaciteit (kWp)"   width="100px" required="true"  default="5.0"/>
        <param field="Mode6"   label="Debug" width="100px">
            <options>
                <option label="Uit" value="Normal" default="true"/>
                <option label="Aan" value="Debug"/>
            </options>
        </param>
    </params>
</plugin>
"""

import Domoticz  # noqa — beschikbaar in Domoticz plugin runtime

UNIT_TODAY    = 1
UNIT_TOMORROW = 2
UNIT_HOUR     = 3


class BasePlugin:
    def __init__(self):
        self.lat         = 52.3676
        self.lon         = 4.9041
        self.tilt        = 35
        self.compass_az  = 180   # Noord=0, Oost=90, Zuid=180, West=270
        self.kwp         = 5.0
        self.debug       = False
        self.error_count = 0

    def onStart(self):
        self.lat        = float(Parameters.get("Address", "52.3676") or 52.3676)
        self.lon        = float(Parameters.get("Port",    "4.9041")  or 4.9041)
        self.tilt       = int(float(Parameters.get("Mode1", "35")    or 35))
        self.compass_az = int(float(Parameters.get("Mode2", "180")   or 180))
        self.kwp        = float(Parameters.get("Mode3", "5.0")       or 5.0)
        self.debug      = Parameters.get("Mode6") == "Debug"

        # forecast.solar gebruikt Zuid=0, Oost=-90, West=90 (compas - 180)
        self._forecast_az = self.compass_az - 180

        self._ensure_devices()

        Domoticz.Heartbeat(3600)  # elk uur ophalen
        Domoticz.Log(
            "forecast.solar gestart — {}, {} | {} kWp | {}° helling | {}° oriëntatie".format(
                self.lat, self.lon, self.kwp, self.tilt, self.compass_az
            )
        )
        self.onHeartbeat()  # direct eerste fetch

    def onHeartbeat(self):
        result = self._fetch()
        if result is None:
            return

        now      = datetime.now()
        today    = now.strftime("%Y-%m-%d")
        tomorrow = (now + timedelta(days=1)).strftime("%Y-%m-%d")
        cur_hour = now.strftime("%Y-%m-%d %H:00")

        wh_day = result.get("watt_hours_day", {})
        watts  = result.get("watts", {})

        today_kwh    = round(wh_day.get(today,    0) / 1000, 2)
        tomorrow_kwh = round(wh_day.get(tomorrow, 0) / 1000, 2)
        hour_w       = int(watts.get(cur_hour, 0))

        if UNIT_TODAY    in Devices: Devices[UNIT_TODAY].Update(nValue=0,    sValue=str(today_kwh))
        if UNIT_TOMORROW in Devices: Devices[UNIT_TOMORROW].Update(nValue=0, sValue=str(tomorrow_kwh))
        if UNIT_HOUR     in Devices: Devices[UNIT_HOUR].Update(nValue=0,     sValue=str(hour_w))

        if self.debug:
            Domoticz.Log("Forecast: vandaag {} kWh | morgen {} kWh | dit uur {} W".format(
                today_kwh, tomorrow_kwh, hour_w
            ))

    # ── Intern ──────────────────────────────────────────────────────────────

    def _ensure_devices(self):
        if UNIT_TODAY not in Devices:
            Domoticz.Device(
                Name="Verwachte opbrengst vandaag",
                Unit=UNIT_TODAY,
                TypeName="Custom",
                Options={"Custom": "1;kWh"},
            ).Create()
        if UNIT_TOMORROW not in Devices:
            Domoticz.Device(
                Name="Verwachte opbrengst morgen",
                Unit=UNIT_TOMORROW,
                TypeName="Custom",
                Options={"Custom": "1;kWh"},
            ).Create()
        if UNIT_HOUR not in Devices:
            Domoticz.Device(
                Name="Verwacht huidig uur",
                Unit=UNIT_HOUR,
                TypeName="Custom",
                Options={"Custom": "1;W"},
            ).Create()

    def _fetch(self):
        url = "https://api.forecast.solar/estimate/{}/{}/{}/{}/{}".format(
            self.lat, self.lon, self.tilt, self._forecast_az, self.kwp
        )
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "DomoticzPlugin/ForecastSolar deeljeenergie.nl"})
            with urllib.request.urlopen(req, timeout=15) as resp:
                return json.loads(resp.read().decode()).get("result", {})
        except urllib.error.HTTPError as e:
            self._log_error("forecast.solar HTTP {}: {}".format(e.code, e.reason))
        except Exception as e:
            self._log_error("forecast.solar niet bereikbaar: " + str(e))
        return None

    def _log_error(self, msg):
        self.error_count += 1
        if self.error_count == 1 or self.error_count % 6 == 0:
            Domoticz.Error(msg)


_plugin = BasePlugin()


def onStart():
    _plugin.onStart()


def onHeartbeat():
    _plugin.onHeartbeat()
Domoticz plugin

P1-meter koppeling met Deel je energie

Stuurt elke 10 seconden P1-meterdata (verbruik, teruglevering, zonneopwekking) naar het Deel je energie platform voor realtime inzicht en energiedeling.

📊
Verbruik (W) Actueel stroomverbruik uit het net.
↩️
Teruglevering (W) Actuele teruglevering aan het net.
☀️
Zonnepanelen opwekking (W) Optioneel: koppel een apart solar-device.

Installatie

  1. Voer php bin/seed-meter.php "Mijn huis" uit om een API-token aan te maken
  2. Noteer het gegenereerde token
  3. Kopieer de map DeeljeEnergieMeter/ naar <domoticz>/plugins/
  4. Herstart Domoticz
  5. Setup → Hardware → toevoegen → "Deel je energie P1 Meter"
  6. Vul API URL, token en het IDX van je P1 Smart Meter device in
  7. Activeer — data stroomt binnen 10 seconden in het admin-dashboard

Configuratieparameters

Parameter Voorbeeld Toelichting
API URL https://deeljeenergie.nl URL van de Deel je energie server
API Token abc123… Token uit php bin/seed-meter.php
Domoticz URL http://localhost:8080 Lokale URL van Domoticz zelf
P1 Device IDX 1 IDX van het DSMR P1 Smart Meter device in Domoticz
Zonnepanelen IDX 0 IDX van solar device, 0 = geen
Domoticz gebruikersnaam Optioneel, bij beveiligde Domoticz-installatie
Domoticz wachtwoord Optioneel, bij beveiligde Domoticz-installatie

Het IDX van een device vind je in Domoticz via Devices → kolom IDX. Voer het seed-script uit op de server waar deeljeenergie.nl op draait.

DeeljeEnergieMeter/plugin.py
"""
Deel je energie — P1 Meter Plugin
===================================
Stuurt elke 10 seconden P1-meterdata (en optioneel zonnepanelen) naar de Deel je energie API.

Installatie:
  1. Kopieer deze map naar <domoticz>/plugins/DeeljeEnergieMeter/
  2. Herstart Domoticz
  3. Ga naar Setup → Hardware → voeg toe: "Deel je energie P1 Meter"
  4. Vul de configuratie in en activeer

Vereisten:
  - Python 3.x (standaard meegeleverd met Domoticz)
  - DSMR P1 hardware al geconfigureerd in Domoticz (levert een Smart Meter device)
"""

import base64
import json
import urllib.request
import urllib.error

# Domoticz plugin metadata
"""
<plugin key="DeeljeEnergieMeter" name="Deel je energie P1 Meter" author="deeljeenergie.nl" version="1.1.0">
    <description>
        Stuurt P1-meterdata elke 10 seconden naar de Deel je energie API.
        Optioneel: ook opwekking van zonnepanelen meesturen via een apart Domoticz device.
    </description>
    <params>
        <param field="Address" label="API URL" width="300px" required="true" default="https://deeljeenergie.nl"/>
        <param field="Mode1" label="API Token (X-API-Key)" width="300px" required="true" default=""/>
        <param field="Mode2" label="Domoticz URL" width="300px" required="true" default="http://localhost:8080"/>
        <param field="Mode3" label="P1 Smart Meter device IDX" width="100px" required="true" default="1"/>
        <param field="Port"  label="Zonnepanelen device IDX (0 = geen)" width="100px" required="false" default="0"/>
        <param field="Mode4" label="Domoticz gebruikersnaam" width="200px" required="false" default=""/>
        <param field="Mode5" label="Domoticz wachtwoord" width="200px" required="false" password="true" default=""/>
        <param field="Mode6" label="Debug" width="100px">
            <options>
                <option label="Uit" value="Normal" default="true"/>
                <option label="Aan" value="Debug"/>
            </options>
        </param>
    </params>
</plugin>
"""

import Domoticz  # noqa — beschikbaar in Domoticz plugin runtime


class BasePlugin:
    def __init__(self):
        self.api_url       = ""
        self.api_token     = ""
        self.domoticz_url  = ""
        self.device_idx    = 1
        self.solar_idx     = 0   # 0 = geen zonnepanelen
        self.domoticz_auth = None
        self.debug         = False
        self.error_count   = 0

    def onStart(self):
        self.api_url      = Parameters["Address"].rstrip("/")
        self.api_token    = Parameters["Mode1"].strip()
        self.domoticz_url = Parameters["Mode2"].rstrip("/")
        self.device_idx   = int(Parameters["Mode3"])
        self.debug        = Parameters["Mode6"] == "Debug"

        port_val = Parameters.get("Port", "0").strip()
        self.solar_idx = int(port_val) if port_val.isdigit() else 0

        user = Parameters["Mode4"].strip()
        pwd  = Parameters["Mode5"].strip()
        if user:
            credentials = base64.b64encode("{}:{}".format(user, pwd).encode()).decode()
            self.domoticz_auth = "Basic " + credentials

        Domoticz.Heartbeat(10)
        solar_info = " | Zonnepanelen IDX: {}".format(self.solar_idx) if self.solar_idx else ""
        Domoticz.Log("Deel je energie P1 plugin gestart. API: {}{}".format(self.api_url, solar_info))

    def onHeartbeat(self):
        reading = self._read_device()
        if reading is None:
            return

        if self.solar_idx:
            solar_w = self._read_solar()
            if solar_w is not None:
                reading["solar_w"] = solar_w

        self._send_reading(reading)

    # ── P1 meter ────────────────────────────────────────────────────────────

    def _read_device(self):
        """Leest het P1 Smart Meter device uit Domoticz via de JSON API."""
        url = "{}/json.htm?type=command&param=getdevices&rid={}".format(self.domoticz_url, self.device_idx)
        req = urllib.request.Request(url)
        if self.domoticz_auth:
            req.add_header("Authorization", self.domoticz_auth)
        try:
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read().decode())
        except Exception as e:
            self._log_error("Kon Domoticz device niet lezen: " + str(e))
            return None

        if data.get("status") != "OK" or not data.get("result"):
            self._log_error("Onverwacht Domoticz antwoord voor IDX {}: {}".format(self.device_idx, data))
            return None

        return self._parse_p1(data["result"][0])

    def _parse_p1(self, device):
        """
        Parst een Domoticz P1 Smart Meter device.
        Data-veld formaat: "T1_use_Wh;T2_use_Wh;T1_del_Wh;T2_del_Wh;cur_use_W;cur_del_W"
        Voorbeeld:         "2474426;1652237;1169617;3051901;58;1562"
        """
        data_str = device.get("Data", "")
        parts    = data_str.split(";")

        if len(parts) >= 6:
            try:
                reading = {
                    "usage_t1_kwh":   round(int(parts[0]) / 1000, 3),
                    "usage_t2_kwh":   round(int(parts[1]) / 1000, 3),
                    "deliver_t1_kwh": round(int(parts[2]) / 1000, 3),
                    "deliver_t2_kwh": round(int(parts[3]) / 1000, 3),
                    "power_use_w":    int(parts[4]),
                    "power_del_w":    int(parts[5]),
                }
                if self.debug:
                    Domoticz.Log("P1 meting: {}W verbruik, {}W teruglevering".format(
                        reading["power_use_w"], reading["power_del_w"],
                    ))
                return reading
            except (ValueError, IndexError) as e:
                self._log_error("Kon P1 Data-veld niet parsen: '{}' — {}".format(data_str, e))
                return None

        # Fallback: gebruik Usage/UsageDeliv velden
        usage       = device.get("Usage", "")
        usage_deliv = device.get("UsageDeliv", "")
        if usage:
            try:
                return {
                    "power_use_w":    int(float(usage.split()[0])),
                    "power_del_w":    int(float(usage_deliv.split()[0])) if usage_deliv else 0,
                    "usage_t1_kwh":   0.0,
                    "usage_t2_kwh":   0.0,
                    "deliver_t1_kwh": 0.0,
                    "deliver_t2_kwh": 0.0,
                }
            except (ValueError, IndexError):
                pass

        self._log_error("Kon P1 device niet parsen. Data: '{}' (IDX {})".format(data_str, self.device_idx))
        return None

    # ── Zonnepanelen ────────────────────────────────────────────────────────

    def _read_solar(self):
        """
        Leest het zonnepanelen device en geeft het huidige vermogen in W terug.
        Ondersteunde Domoticz device-formaten:
          - Usage/UsageDeliv veld:  "1234 Watt"  of  "1234.5"
          - Data veld (kWh-teller): "vandaag_kWh ; totaal_kWh"  → wordt genegeerd (geen W)
          - Data veld (W+kWh):      "cur_W ; total_Wh"          → eerste deel als W
        """
        url = "{}/json.htm?type=command&param=getdevices&rid={}".format(self.domoticz_url, self.solar_idx)
        req = urllib.request.Request(url)
        if self.domoticz_auth:
            req.add_header("Authorization", self.domoticz_auth)
        try:
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read().decode())
        except Exception as e:
            self._log_error("Kon zonnepanelen device niet lezen (IDX {}): {}".format(self.solar_idx, str(e)))
            return None

        if data.get("status") != "OK" or not data.get("result"):
            self._log_error("Onverwacht Domoticz antwoord voor zonnepanelen IDX {}: {}".format(self.solar_idx, data))
            return None

        device   = data["result"][0]
        solar_w  = self._parse_solar_w(device)

        if self.debug and solar_w is not None:
            Domoticz.Log("Zonnepanelen: {} W (IDX {})".format(solar_w, self.solar_idx))

        return solar_w

    def _parse_solar_w(self, device):
        """
        Probeert het huidige vermogen (W) te lezen uit een Domoticz device.
        Werkt voor de meeste Solar/PV device-types.
        """
        # 1) Usage-veld: "1234 Watt", "1234.5 W", of gewoon "1234"
        usage = device.get("Usage", "").strip()
        if usage:
            try:
                return int(float(usage.split()[0]))
            except (ValueError, IndexError):
                pass

        # 2) Data-veld met meerdere delen: "cur_W;total_Wh" of "cur_W;today_kWh;total_kWh"
        data_str = device.get("Data", "").strip()
        if data_str:
            parts = data_str.split(";")
            # Alleen het eerste deel gebruiken als vermogen als het er als een getal uitziet
            try:
                val = float(parts[0].split()[0])
                # Waarden > 20 000 zijn waarschijnlijk Wh-tellers, geen W — overslaan
                if val <= 20000:
                    return int(val)
            except (ValueError, IndexError):
                pass

        self._log_error("Kon zonnepanelen-vermogen niet parsen. Device: {} (IDX {})".format(
            device.get("Name", "?"), self.solar_idx
        ))
        return None

    # ── Versturen ───────────────────────────────────────────────────────────

    def _send_reading(self, reading):
        """Stuurt de meting naar de Deel je energie API."""
        url     = self.api_url + "/api/meter/reading"
        payload = json.dumps(reading).encode("utf-8")
        req     = urllib.request.Request(
            url,
            data=payload,
            headers={
                "Content-Type": "application/json",
                "X-API-Key":    self.api_token,
            },
            method="POST",
        )

        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                if resp.status == 201:
                    self.error_count = 0
                    if self.debug:
                        solar_str = " | zon: {} W".format(reading.get("solar_w", 0)) if "solar_w" in reading else ""
                        Domoticz.Log("Meting verstuurd: {} W verbruik, {} W teruglevering{}".format(
                            reading["power_use_w"], reading["power_del_w"], solar_str
                        ))
                else:
                    self._log_error("API gaf HTTP {} terug".format(resp.status))
        except urllib.error.HTTPError as e:
            self._log_error("API fout HTTP {}: {}".format(e.code, e.reason))
        except Exception as e:
            self._log_error("Kon API niet bereiken: " + str(e))

    def _log_error(self, msg):
        self.error_count += 1
        # Log alleen elke 6e fout (= 1x per minuut bij 10s heartbeat) om logspam te voorkomen
        if self.error_count == 1 or self.error_count % 6 == 0:
            Domoticz.Error(msg)


_plugin = BasePlugin()


def onStart():
    _plugin.onStart()


def onHeartbeat():
    _plugin.onHeartbeat()
☀️

Wil je weten wat jouw panelen morgen opwekken?

Geen Domoticz nodig — vul je locatie in voor een directe voorspelling via forecast.solar.

☀️ Bereken opbrengst morgen