From 4d811691e91f348217a1855abc7c22f1a6669ac7 Mon Sep 17 00:00:00 2001 From: chingon007 <76529461+chingon007@users.noreply.github.com> Date: Thu, 18 May 2023 00:16:08 +0200 Subject: [PATCH] [sonnen] Update to API V2 of vendor and add PowerMeter (#14589) * Implementing sonnen APi V2 * Fixed issues with powermeter and added two more channels from consumption. Signed-off-by: chingon007 --- bundles/org.openhab.binding.sonnen/README.md | 8 +- .../internal/SonnenBindingConstants.java | 6 ++ .../sonnen/internal/SonnenConfiguration.java | 1 + .../sonnen/internal/SonnenHandler.java | 71 +++++++++++++++++- .../SonnenJSONCommunication.java | 74 ++++++++++++++++++- .../communication/SonnenJsonDataDTO.java | 2 +- .../SonnenJsonPowerMeterDataDTO.java | 46 ++++++++++++ .../resources/OH-INF/i18n/sonnen.properties | 10 +++ .../resources/OH-INF/thing/thing-types.xml | 38 ++++++++++ .../resources/OH-INF/update/instructions.xml | 23 ++++++ 10 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonPowerMeterDataDTO.java create mode 100644 bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/update/instructions.xml diff --git a/bundles/org.openhab.binding.sonnen/README.md b/bundles/org.openhab.binding.sonnen/README.md index a5888ccb0..90b0156f9 100644 --- a/bundles/org.openhab.binding.sonnen/README.md +++ b/bundles/org.openhab.binding.sonnen/README.md @@ -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 diff --git a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenBindingConstants.java b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenBindingConstants.java index f83edf95e..b2a0bdef4 100644 --- a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenBindingConstants.java +++ b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenBindingConstants.java @@ -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"; } diff --git a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenConfiguration.java b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenConfiguration.java index 0169ca766..bb46a4946 100644 --- a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenConfiguration.java +++ b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenConfiguration.java @@ -25,4 +25,5 @@ public class SonnenConfiguration { public @Nullable String hostIP = null; public int refreshInterval = 30; + public String authToken = ""; } diff --git a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenHandler.java b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenHandler.java index 092813c0d..4eb13120d 100644 --- a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenHandler.java +++ b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/SonnenHandler.java @@ -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 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(dataPM[0].getKwhImported(), Units.KILOWATT_HOUR); + update(state, channelId); + break; + case CHANNELENERGYEXPORTEDSTATEPRODUCTION: + state = new QuantityType(dataPM[0].getKwhExported(), Units.KILOWATT_HOUR); + update(state, channelId); + break; + case CHANNELENERGYIMPORTEDSTATECONSUMPTION: + state = new QuantityType(dataPM[1].getKwhImported(), Units.KILOWATT_HOUR); + update(state, channelId); + break; + case CHANNELENERGYEXPORTEDSTATECONSUMPTION: + state = new QuantityType(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; } } diff --git a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJSONCommunication.java b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJSONCommunication.java index f662aaf16..baa58e690 100644 --- a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJSONCommunication.java +++ b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJSONCommunication.java @@ -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/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; + } } diff --git a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonDataDTO.java b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonDataDTO.java index 3f53aa7ed..8d25eddc5 100644 --- a/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonDataDTO.java +++ b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonDataDTO.java @@ -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 index 000000000..31ca1718e --- /dev/null +++ b/bundles/org.openhab.binding.sonnen/src/main/java/org/openhab/binding/sonnen/internal/communication/SonnenJsonPowerMeterDataDTO.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/i18n/sonnen.properties b/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/i18n/sonnen.properties index 48cb6622b..743806fd6 100644 --- a/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/i18n/sonnen.properties +++ b/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/i18n/sonnen.properties @@ -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 diff --git a/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/thing/thing-types.xml index 5821dcf89..705804c20 100644 --- a/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/thing/thing-types.xml @@ -25,7 +25,15 @@ + + + + + + sonnen + 1 + @@ -33,6 +41,12 @@ Please add the IP Address of your sonnen battery. + + service + + 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. + How often in seconds the sonnen battery should schedule a refresh after a channel is linked to an item. @@ -128,4 +142,28 @@ Indicates if there is a current flow from production towards grid. + + Number:Energy + + Indicates the imported kWh Production. + + + + Number:Energy + + Indicates the exported kWh Production. + + + + Number:Energy + + Indicates the imported kWh Consumption. + + + + Number:Energy + + Indicates the exported kWh Consumption. + + 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 index 000000000..61699a648 --- /dev/null +++ b/bundles/org.openhab.binding.sonnen/src/main/resources/OH-INF/update/instructions.xml @@ -0,0 +1,23 @@ + + + + + + + + sonnen:energyImportedStateProduction + + + sonnen:energyExportedStateProduction + + + sonnen:energyImportedStateConsumption + + + sonnen:energyExportedStateConsumption + + + +