]> git.basschouten.com Git - openhab-addons.git/commitdiff
[sonnen] Update to API V2 of vendor and add PowerMeter (#14589)
authorchingon007 <76529461+chingon007@users.noreply.github.com>
Wed, 17 May 2023 22:16:08 +0000 (00:16 +0200)
committerGitHub <noreply@github.com>
Wed, 17 May 2023 22:16:08 +0000 (00:16 +0200)
* Implementing sonnen APi V2
* Fixed issues with powermeter and added two more channels from consumption.

Signed-off-by: chingon007 <tron81@gmx.de>
bundles/org.openhab.binding.sonnen/README.md
bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenBindingConstants.java
bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenConfiguration.java
bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenHandler.java
bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJSONCommunication.java
bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonDataDTO.java
bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonPowerMeterDataDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/i18n/sonnen.properties
bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/update/instructions.xml [new file with mode: 0644]

index a5888ccb06f9ea22348b540392ccf7ca3847f356..90b0156f96f3975e18ac605670f4f4f0bc7629f9 100644 (file)
@@ -2,6 +2,8 @@
 
 The binding for sonnen communicates with a sonnen battery.
 More information about the sonnen battery can be found [here](https://sonnen.de/).
+The binding supports the old deprecated V1 from sonnen as well as V2 which requires an authentication token.
+More information about the V2 API can be found at `http://LOCAL-SONNENBATTERY-SYSTEM-IP/api/doc.html`
 
 ## Supported Things
 
@@ -12,6 +14,7 @@ More information about the sonnen battery can be found [here](https://sonnen.de/
 ## Thing Configuration
 
 Only the parameter `hostIP` is required; this is the IP address of the sonnen battery in your local network.
+If you want to use the V2 API, which supports more channels, you need to provide the `authToken`.
 
 ## Channels
 
@@ -35,7 +38,10 @@ The following channels are yet supported:
 | flowConsumptionProductionState | Switch        | read   | Indicates if there is a current flow from Solar Production towards Consumption          |
 | flowGridBatteryState           | Switch        | read   | Indicates if there is a current flow from Grid towards Battery                          |
 | flowProductionBatteryState     | Switch        | read   | Indicates if there is a current flow from Production towards Battery                    |
-| flowProductionGridState        | Switch        | read   | Indicates if there is a current flow from Production towards Grid                       |
+| energyImportedStateProduction  | Number:Energy | read   | Indicates the imported kWh Production                                                   |
+| energyExportedStateProduction  | Number:Energy | read   | Indicates the exported kWh Production                                                   |
+| energyImportedStateConsumption | Number:Energy | read   | Indicates the imported kWh Consumption                                                  |
+| energyExportedStateConsumption | Number:Energy | read   | Indicates the exported kWh Consumption                                                  |
 
 ## Full Example
 
index f83edf95ed73f8c8d92efe89ce732ef6652d7cae..b2a0bdef43944e7962f1bbb836a67dd00ced44cc 100644 (file)
@@ -45,4 +45,10 @@ public class SonnenBindingConstants {
     public static final String CHANNELFLOWGRIDBATTERYSTATE = "flowGridBatteryState";
     public static final String CHANNELFLOWPRODUCTIONBATTERYSTATE = "flowProductionBatteryState";
     public static final String CHANNELFLOWPRODUCTIONGRIDSTATE = "flowProductionGridState";
+
+    // List of new Channel ids for PowerMeter API
+    public static final String CHANNELENERGYIMPORTEDSTATEPRODUCTION = "energyImportedStateProduction";
+    public static final String CHANNELENERGYEXPORTEDSTATEPRODUCTION = "energyExportedStateProduction";
+    public static final String CHANNELENERGYIMPORTEDSTATECONSUMPTION = "energyImportedStateConsumption";
+    public static final String CHANNELENERGYEXPORTEDSTATECONSUMPTION = "energyExportedStateConsumption";
 }
index 0169ca76695e02839408376665af53beed22060a..bb46a494642d9c10df0e07dc1eed9e46c42dcd6f 100644 (file)
@@ -25,4 +25,5 @@ public class SonnenConfiguration {
 
     public @Nullable String hostIP = null;
     public int refreshInterval = 30;
+    public String authToken = "";
 }
index 092813c0d355b66fa9f8088e5ceeec8072301caa..4eb13120d17610d8bcdd884b407a3c1928fcccb5 100644 (file)
@@ -20,12 +20,14 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Energy;
 import javax.measure.quantity.Power;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.sonnen.internal.communication.SonnenJSONCommunication;
 import org.openhab.binding.sonnen.internal.communication.SonnenJsonDataDTO;
+import org.openhab.binding.sonnen.internal.communication.SonnenJsonPowerMeterDataDTO;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.library.unit.Units;
@@ -61,6 +63,10 @@ public class SonnenHandler extends BaseThingHandler {
 
     private boolean automaticRefreshing = false;
 
+    private boolean sonnenAPIV2 = false;
+
+    private int disconnectionCounter = 0;
+
     private Map<String, Boolean> linkedChannels = new HashMap<>();
 
     public SonnenHandler(Thing thing) {
@@ -82,6 +88,10 @@ public class SonnenHandler extends BaseThingHandler {
             return;
         }
 
+        if (!config.authToken.isEmpty()) {
+            sonnenAPIV2 = true;
+        }
+
         serviceCommunication.setConfig(config);
         updateStatus(ThingStatus.UNKNOWN);
         scheduler.submit(() -> {
@@ -101,13 +111,23 @@ public class SonnenHandler extends BaseThingHandler {
      * @return true if the update succeeded, false otherwise
      */
     private boolean updateBatteryData() {
-        String error = serviceCommunication.refreshBatteryConnection();
+        String error = "";
+        if (sonnenAPIV2) {
+            error = serviceCommunication.refreshBatteryConnectionAPICALLV2(arePowerMeterChannelsLinked());
+        } else {
+            error = serviceCommunication.refreshBatteryConnectionAPICALLV1();
+        }
         if (error.isEmpty()) {
             if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
                 updateStatus(ThingStatus.ONLINE);
+                disconnectionCounter = 0;
             }
         } else {
+            disconnectionCounter++;
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
+            if (disconnectionCounter < 60) {
+                return true;
+            }
         }
         return error.isEmpty();
     }
@@ -134,7 +154,7 @@ public class SonnenHandler extends BaseThingHandler {
     }
 
     /**
-     * Start the job refreshing the oven status
+     * Start the job refreshing the battery status
      */
     private void startAutomaticRefresh() {
         ScheduledFuture<?> job = refreshJob;
@@ -176,6 +196,35 @@ public class SonnenHandler extends BaseThingHandler {
         if (isLinked(channelId)) {
             State state = null;
             SonnenJsonDataDTO data = serviceCommunication.getBatteryData();
+            // The sonnen API has two sub-channels, e.g. 4_1 and 4_2, one representing consumption and the
+            // other production. E.g. 4_1.kwh_imported represents the total production since the
+            // battery was installed.
+            SonnenJsonPowerMeterDataDTO[] dataPM = null;
+            if (arePowerMeterChannelsLinked()) {
+                dataPM = serviceCommunication.getPowerMeterData();
+            }
+
+            if (dataPM != null && dataPM.length >= 2) {
+                switch (channelId) {
+                    case CHANNELENERGYIMPORTEDSTATEPRODUCTION:
+                        state = new QuantityType<Energy>(dataPM[0].getKwhImported(), Units.KILOWATT_HOUR);
+                        update(state, channelId);
+                        break;
+                    case CHANNELENERGYEXPORTEDSTATEPRODUCTION:
+                        state = new QuantityType<Energy>(dataPM[0].getKwhExported(), Units.KILOWATT_HOUR);
+                        update(state, channelId);
+                        break;
+                    case CHANNELENERGYIMPORTEDSTATECONSUMPTION:
+                        state = new QuantityType<Energy>(dataPM[1].getKwhImported(), Units.KILOWATT_HOUR);
+                        update(state, channelId);
+                        break;
+                    case CHANNELENERGYEXPORTEDSTATECONSUMPTION:
+                        state = new QuantityType<Energy>(dataPM[1].getKwhExported(), Units.KILOWATT_HOUR);
+                        update(state, channelId);
+                        break;
+                }
+            }
+
             if (data != null) {
                 switch (channelId) {
                     case CHANNELBATTERYDISCHARGINGSTATE:
@@ -234,9 +283,23 @@ public class SonnenHandler extends BaseThingHandler {
                         update(OnOffType.from(data.isFlowProductionGrid()), channelId);
                         break;
                 }
-            } else {
-                update(null, channelId);
             }
+        } else {
+            update(null, channelId);
+        }
+    }
+
+    private boolean arePowerMeterChannelsLinked() {
+        if (isLinked(CHANNELENERGYIMPORTEDSTATEPRODUCTION)) {
+            return true;
+        } else if (isLinked(CHANNELENERGYEXPORTEDSTATEPRODUCTION)) {
+            return true;
+        } else if (isLinked(CHANNELENERGYIMPORTEDSTATECONSUMPTION)) {
+            return true;
+        } else if (isLinked(CHANNELENERGYEXPORTEDSTATECONSUMPTION)) {
+            return true;
+        } else {
+            return false;
         }
     }
 
index f662aaf167fc1b660598094724f463f7b530cb46..baa58e6907a1e3c2c257a187543ba6ea1862f7c6 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.sonnen.internal.communication;
 
 import java.io.IOException;
+import java.util.Properties;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -38,6 +39,7 @@ public class SonnenJSONCommunication {
 
     private Gson gson;
     private @Nullable SonnenJsonDataDTO batteryData;
+    private SonnenJsonPowerMeterDataDTO @Nullable [] powerMeterData;
 
     public SonnenJSONCommunication() {
         gson = new Gson();
@@ -45,14 +47,56 @@ public class SonnenJSONCommunication {
     }
 
     /**
-     * Refreshes the battery connection.
+     * Refreshes the battery connection with the new API Call V2.
      *
      * @return an empty string if no error occurred, the error message otherwise.
      */
-    public String refreshBatteryConnection() {
+    public String refreshBatteryConnectionAPICALLV2(boolean powerMeter) {
         String result = "";
-        String urlStr = "http://" + config.hostIP + "/api/v1/status";
+        String urlStr = "http://" + config.hostIP + "/api/v2/status";
+        Properties httpHeader = new Properties();
+        httpHeader = createHeader(config.authToken);
+        try {
+            String response = HttpUtil.executeUrl("GET", urlStr, httpHeader, null, "application/json", 10000);
+            logger.debug("BatteryData = {}", response);
+            if (response == null) {
+                throw new IOException("HttpUtil.executeUrl returned null");
+            }
+            batteryData = gson.fromJson(response, SonnenJsonDataDTO.class);
+
+            if (powerMeter) {
+                response = HttpUtil.executeUrl("GET", "http://" + config.hostIP + "/api/v2/powermeter", httpHeader,
+                        null, "application/json", 10000);
+                logger.debug("PowerMeterData = {}", response);
+                if (response == null) {
+                    throw new IOException("HttpUtil.executeUrl returned null");
+                }
+
+                powerMeterData = gson.fromJson(response, SonnenJsonPowerMeterDataDTO[].class);
+            }
+        } catch (IOException | JsonSyntaxException e) {
+            logger.debug("Error processiong Get request {}:  {}", urlStr, e.getMessage());
+            String message = e.getMessage();
+            if (message != null && message.contains("WWW-Authenticate header")) {
+                result = "Given token: " + config.authToken + " is not valid.";
+            } else {
+                result = "Cannot find service on given IP " + config.hostIP + ". Please verify the IP address!";
+                logger.debug("Error in establishing connection: {}", e.getMessage());
+            }
+            batteryData = null;
+            powerMeterData = new SonnenJsonPowerMeterDataDTO[] {};
+        }
+        return result;
+    }
 
+    /**
+     * Refreshes the battery connection with the Old API Call.
+     *
+     * @return an empty string if no error occurred, the error message otherwise.
+     */
+    public String refreshBatteryConnectionAPICALLV1() {
+        String result = "";
+        String urlStr = "http://" + config.hostIP + "/api/v1/status";
         try {
             String response = HttpUtil.executeUrl("GET", urlStr, 10000);
             logger.debug("BatteryData = {}", response);
@@ -85,4 +129,28 @@ public class SonnenJSONCommunication {
     public @Nullable SonnenJsonDataDTO getBatteryData() {
         return this.batteryData;
     }
+
+    /**
+     * Returns the actual stored Power Meter Data Array
+     *
+     * @return JSON Data from the Power Meter or null if request failed
+     */
+    public SonnenJsonPowerMeterDataDTO @Nullable [] getPowerMeterData() {
+        return this.powerMeterData;
+    }
+
+    /**
+     * Creates the header for the Get Request
+     *
+     * @return The created Header Properties
+     */
+    private Properties createHeader(String authToken) {
+        Properties httpHeader = new Properties();
+        httpHeader.setProperty("Host", config.hostIP);
+        httpHeader.setProperty("Accept", "*/*");
+        httpHeader.setProperty("Proxy-Connection", "keep-alive");
+        httpHeader.setProperty("Auth-Token", authToken);
+        httpHeader.setProperty("Accept-Encoding", "gzip;q=1.0, compress;q=0.5");
+        return httpHeader;
+    }
 }
index 3f53aa7ed3fa6d6f173f99a42cc96fe982e66370..8d25eddc579ce5f928ea398f91c15de5b48be345 100644 (file)
@@ -16,7 +16,7 @@ import com.google.gson.annotations.SerializedName;
 
 /**
  * The {@link SonnenJsonDataDTO} is the Java class used to map the JSON
- * response to an Oven request.
+ * response to an Object.
  *
  * @author Christian Feininger - Initial contribution
  */
diff --git a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonPowerMeterDataDTO.java b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonPowerMeterDataDTO.java
new file mode 100644 (file)
index 0000000..31ca171
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.sonnen.internal.communication;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link SonnenJsonPowerMeterDataDTO} is the Java class used to map the JSON
+ * response from the API to a PowerMeter Object.
+ *
+ * @author Christian Feininger - Initial contribution
+ */
+@NonNullByDefault
+public class SonnenJsonPowerMeterDataDTO {
+
+    @SerializedName("kwh_exported")
+    private float kwhExported;
+    @SerializedName("kwh_imported")
+    private float kwhImported;
+
+    /**
+     * @return the kwh_exported
+     */
+    public float getKwhExported() {
+        return kwhExported;
+    }
+
+    /**
+     * @return the kwh_imported
+     */
+    public float getKwhImported() {
+        return kwhImported;
+    }
+}
index 48cb6622b5f9b39918f812e7854f521bc89e5932..743806fd67c10f4789e8280dc16cb4c35c2b8663 100644 (file)
@@ -14,6 +14,8 @@ thing-type.config.sonnen.sonnenbattery.hostIP.label = IP Address
 thing-type.config.sonnen.sonnenbattery.hostIP.description = Please add the IP Address of your sonnen battery.
 thing-type.config.sonnen.sonnenbattery.refreshInterval.label = Refresh Interval
 thing-type.config.sonnen.sonnenbattery.refreshInterval.description = How often in seconds the sonnen battery should schedule a refresh after a channel is linked to an item. Valid input is 0 - 1000.
+thing-type.config.sonnen.sonnenbattery.authToken.label = Authentication Token
+thing-type.config.sonnen.sonnenbattery.authToken.description = Authentication Token which can be found under "Software Integration" if you connect locally to your sonnen battery. If empty the old deprecated API will be used.
 
 # channel types
 
@@ -45,3 +47,11 @@ channel-type.sonnen.gridFeedIn.label = Grid Feed In
 channel-type.sonnen.gridFeedIn.description = Indicates the actual current feeding to the Grid. Otherwise 0.
 channel-type.sonnen.solarProduction.label = Solar Production
 channel-type.sonnen.solarProduction.description = Indicates the actual production of the Solar system.
+channel-type.sonnen.energyImportedStateProduction.label = Imported kWh Production.
+channel-type.sonnen.energyImportedStateProduction.description = Indicates the imported kWh Production 
+channel-type.sonnen.energyExportedStateProduction.label= Exported kWh Production.
+channel-type.sonnen.energyExportedStateProduction.description = Indicates the exported kWh Production
+channel-type.sonnen.energyImportedStateConsumption.label = Imported kWh Consumption.
+channel-type.sonnen.energyImportedStateConsupmtion.description = Indicates the imported kWh Consumption 
+channel-type.sonnen.energyExportedStateConsumption.label= Exported kWh Consumption.
+channel-type.sonnen.energyExportedStateConsumption.description = Indicates the exported kWh Consumption
index 5821dcf895244ff51acd2d1e86e49412b0ab6480..705804c20508db2477abafcf0ecc8562bda69641 100644 (file)
                        <channel id="flowGridBatteryState" typeId="flowGridBatteryState"/>
                        <channel id="flowProductionBatteryState" typeId="flowProductionBatteryState"/>
                        <channel id="flowProductionGridState" typeId="flowProductionGridState"/>
+                       <channel id="energyImportedStateProduction" typeId="energyImportedStateProduction"/>
+                       <channel id="energyExportedStateProduction" typeId="energyExportedStateProduction"/>
+                       <channel id="energyImportedStateConsumption" typeId="energyImportedStateConsumption"/>
+                       <channel id="energyExportedStateConsumption" typeId="energyExportedStateConsumption"/>
                </channels>
+               <properties>
+                       <property name="vendor">sonnen</property>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
 
                <config-description>
                        <parameter name="hostIP" type="text" required="true">
                                <label>IP Address</label>
                                <description>Please add the IP Address of your sonnen battery.</description>
                        </parameter>
+                       <parameter name="authToken" type="text">
+                               <context>service</context>
+                               <label>sonnen Authentication Token</label>
+                               <description>Authentication Token which can be found under "Software Integration" if you connect locally to your
+                                       sonnen battery. If empty the old deprecated API will be used.</description>
+                       </parameter>
                        <parameter name="refreshInterval" type="integer" unit="s" min="0" max="1000">
                                <label>Refresh Interval</label>
                                <description>How often in seconds the sonnen battery should schedule a refresh after a channel is linked to an item.
                <description>Indicates if there is a current flow from production towards grid.</description>
                <state readOnly="true"/>
        </channel-type>
+       <channel-type id="energyImportedStateProduction">
+               <item-type>Number:Energy</item-type>
+               <label>kWh Imported Production</label>
+               <description>Indicates the imported kWh Production.</description>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+       <channel-type id="energyExportedStateProduction">
+               <item-type>Number:Energy</item-type>
+               <label>kWh Exported Production</label>
+               <description>Indicates the exported kWh Production.</description>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+       <channel-type id="energyImportedStateConsumption">
+               <item-type>Number:Energy</item-type>
+               <label>kWh Imported Consumption</label>
+               <description>Indicates the imported kWh Consumption.</description>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+       <channel-type id="energyExportedStateConsumption">
+               <item-type>Number:Energy</item-type>
+               <label>kWh Exported Consumption</label>
+               <description>Indicates the exported kWh Consumption.</description>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
 </thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/update/instructions.xml
new file mode 100644 (file)
index 0000000..61699a6
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
+
+       <thing-type uid="sonnen:sonnenbattery">
+
+               <instruction-set targetVersion="1">
+                       <add-channel id="energyImportedStateProduction">
+                               <type>sonnen:energyImportedStateProduction</type>
+                       </add-channel>
+                       <add-channel id="energyExportedStateProduction">
+                               <type>sonnen:energyExportedStateProduction</type>
+                       </add-channel>
+                       <add-channel id="energyImportedStateConsumption">
+                               <type>sonnen:energyImportedStateConsumption</type>
+                       </add-channel>
+                       <add-channel id="energyExportedStateConsumption">
+                               <type>sonnen:energyExportedStateConsumption</type>
+                       </add-channel>
+               </instruction-set>
+       </thing-type>
+</update:update-descriptions>