[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 <tron81@gmx.de>
This commit is contained in:
chingon007 2023-05-18 00:16:08 +02:00 committed by GitHub
parent 6ab8111f9e
commit 4d811691e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 270 additions and 9 deletions

View 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

View 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";
}

View File

@ -25,4 +25,5 @@ public class SonnenConfiguration {
public @Nullable String hostIP = null;
public int refreshInterval = 30;
public String authToken = "";
}

View 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;
}
}

View 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/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;
}
}

View 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
*/

View File

@ -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;
}
}

View 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

View File

@ -25,7 +25,15 @@
<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">
@ -33,6 +41,12 @@
<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.
@ -128,4 +142,28 @@
<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>

View File

@ -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>