From 66a540289d6e10beb0523db165657c2d298c24ac Mon Sep 17 00:00:00 2001 From: Iwan Bron <44524316+itb3@users.noreply.github.com> Date: Sun, 23 May 2021 20:45:04 +0200 Subject: [PATCH] [semsportal] initial contribution (#10100) Signed-off-by: Iwan Bron --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.semsportal/NOTICE | 13 + .../org.openhab.binding.semsportal/README.md | 60 ++ .../org.openhab.binding.semsportal/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/CommunicationException.java | 31 + .../internal/ConfigurationException.java | 30 + .../semsportal/internal/PortalHandler.java | 229 +++++ .../internal/SEMSPortalBindingConstants.java | 59 ++ .../internal/SEMSPortalConfiguration.java | 36 + .../internal/SEMSPortalHandlerFactory.java | 75 ++ .../semsportal/internal/StateHelper.java | 108 ++ .../semsportal/internal/StationHandler.java | 178 ++++ .../discovery/StationDiscoveryService.java | 78 ++ .../semsportal/internal/dto/BaseResponse.java | 56 ++ .../internal/dto/InverterDetails.java | 31 + .../dto/KeyPerformanceIndicators.java | 65 ++ .../semsportal/internal/dto/LoginRequest.java | 48 + .../internal/dto/LoginResponse.java | 29 + .../semsportal/internal/dto/SEMSToken.java | 81 ++ .../semsportal/internal/dto/Station.java | 87 ++ .../internal/dto/StationListRequest.java | 43 + .../internal/dto/StationListResponse.java | 32 + .../internal/dto/StationStatus.java | 68 ++ .../internal/dto/StatusRequest.java | 38 + .../internal/dto/StatusResponse.java | 30 + .../main/resources/OH-INF/binding/binding.xml | 14 + .../resources/OH-INF/thing/thing-types.xml | 96 ++ .../internal/SEMSJsonParserTest.java | 104 ++ .../src/test/resources/error_login.json | 13 + .../src/test/resources/error_status.json | 17 + .../src/test/resources/success_list.json | 73 ++ .../src/test/resources/success_login.json | 21 + .../src/test/resources/success_status.json | 941 ++++++++++++++++++ bundles/pom.xml | 1 + 36 files changed, 2817 insertions(+) create mode 100644 bundles/org.openhab.binding.semsportal/NOTICE create mode 100644 bundles/org.openhab.binding.semsportal/README.md create mode 100644 bundles/org.openhab.binding.semsportal/pom.xml create mode 100644 bundles/org.openhab.binding.semsportal/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/CommunicationException.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/ConfigurationException.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/PortalHandler.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalBindingConstants.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalConfiguration.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalHandlerFactory.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StateHelper.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StationHandler.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/discovery/StationDiscoveryService.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/BaseResponse.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/InverterDetails.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/KeyPerformanceIndicators.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginRequest.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginResponse.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/SEMSToken.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/Station.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListRequest.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListResponse.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationStatus.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusRequest.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusResponse.java create mode 100644 bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.semsportal/src/test/java/org/openhab/binding/semsportal/internal/SEMSJsonParserTest.java create mode 100644 bundles/org.openhab.binding.semsportal/src/test/resources/error_login.json create mode 100644 bundles/org.openhab.binding.semsportal/src/test/resources/error_status.json create mode 100644 bundles/org.openhab.binding.semsportal/src/test/resources/success_list.json create mode 100644 bundles/org.openhab.binding.semsportal/src/test/resources/success_login.json create mode 100644 bundles/org.openhab.binding.semsportal/src/test/resources/success_status.json diff --git a/CODEOWNERS b/CODEOWNERS index ce4bd7ea6..65619f4bc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -249,6 +249,7 @@ /bundles/org.openhab.binding.sagercaster/ @clinique /bundles/org.openhab.binding.samsungtv/ @paulianttila /bundles/org.openhab.binding.satel/ @druciak +/bundles/org.openhab.binding.semsportal/ @itb3 /bundles/org.openhab.binding.senechome/ @vctender @KorbinianP /bundles/org.openhab.binding.seneye/ @nikotanghe /bundles/org.openhab.binding.sensebox/ @hakan42 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index be4084107..a3396a3a3 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1221,6 +1221,11 @@ org.openhab.binding.satel ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.semsportal + ${project.version} + org.openhab.addons.bundles org.openhab.binding.senechome diff --git a/bundles/org.openhab.binding.semsportal/NOTICE b/bundles/org.openhab.binding.semsportal/NOTICE new file mode 100644 index 000000000..38d625e34 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.semsportal/README.md b/bundles/org.openhab.binding.semsportal/README.md new file mode 100644 index 000000000..1d6929a73 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/README.md @@ -0,0 +1,60 @@ +# SEMSPortal Binding + +This binding can help you include statistics of your SEMS / GoodWe solar panel installation into openHAB. +It is a read-only connection that maps collected parameters to openHAB channels. +It provides current, day, month and total yields, as well as some income statistics if you have configured these in the SEMS portal. +It requires a power station that is connected through the internet to the SEMS portal. + +## Supported Things + +This binding provides two Thing types: a bridge to the SEMS Portal, and the Power Stations which are found at the Portal. +The Portal (semsportal:portal) represents your account in the SEMS portal. +The Power Station (semsportal:station) is an installation of a Power Station or inverter that reports to the SEMS portal and is available to your account. + +## Discovery + +Once you have configured a Portal Bridge, the binding will discover all Power Stations that are available to this account. +You can trigger discovery in the add new Thing section of openHAB. +Select the SEMS binding and press the Scan button. +The discovered Power Stations will appear as new Things. + +## Thing Configuration + +The configuration of the Portal Thing (Bridge) is pretty straight forward. +You need to have your power station set up in the SEMS portal, and you need to have an account that is allowed to view the power station data. +You should log in at least once in the portal with this account to activate it. +The Portal needs the username and password to connect and retrieve the data. +You can configure the update frequency between 1 and 60 minutes. +The default is 5 minutes. + +Power Stations have no settings and will be auto discovered when you add a Portal Bridge. + +## Channels + +The Portal(Bridge) has no channels. +The Power Station Thing has the following channels: + +| channel | type | description | +| ------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- | +| lastUpdate | DateTime | Last time the powerStation sent information to the portal | +| currentOutput | Number:Power | The current output of the powerStation in Watt | +| todayTotal | Number:Energy | Todays total generation of the station in kWh | +| monthTotal | Number:Energy | This month's total generation of the station in kWh | +| overallTotal | Number:Energy | The total generation of the station since installation, in kWh | +| todayIncome | Number | Todays income as reported by the portal, if you have configured the power rates of your energy provider | +| totalIncome | Number | The total income as reported by the portal, if you have configured the power rates of your energy provider | + +## Parameters + +The PowerStation Thing has no parameters. +Only the Bridge has the following configuration parameters: + +| Parameter | Required? | Description | +| ----------- |:---------:| ---------------------------------------------------------------------------------------------------------- | +| username | X | Account name (email address) at the SEMS portal. Account must have been used at least once to log in. | +| password | X | Password of the SEMS portal | +| update | | Number of minutes between two updates. Between 1 and 60 minutes, defaults to 5 minutes | + +## Credits + +This binding has been created using the information provided by RogerG007 in this forum topic: https://community.openhab.org/t/connecting-goodwe-solar-panel-inverter-to-openhab/85480 diff --git a/bundles/org.openhab.binding.semsportal/pom.xml b/bundles/org.openhab.binding.semsportal/pom.xml new file mode 100644 index 000000000..284633860 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.semsportal + + openHAB Add-ons :: Bundles :: SEMSPortal Binding + + diff --git a/bundles/org.openhab.binding.semsportal/src/main/feature/feature.xml b/bundles/org.openhab.binding.semsportal/src/main/feature/feature.xml new file mode 100644 index 000000000..8e6e2ea0b --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.semsportal/${project.version} + + diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/CommunicationException.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/CommunicationException.java new file mode 100644 index 000000000..4b5662970 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/CommunicationException.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception indicating there was a problem communicating with the portal. It can indicate either no response at all, or + * a response that was not expected. + * + * @author Iwan Bron - Initial contribution + * + */ +@NonNullByDefault +public class CommunicationException extends Exception { + private static final long serialVersionUID = 4175625868879971138L; + + public CommunicationException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/ConfigurationException.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/ConfigurationException.java new file mode 100644 index 000000000..311115792 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/ConfigurationException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception indicating that the configuration of the portal was wrong, like an unknown account or invalid password. + * + * @author Iwan Bron - Initial contribution + * + */ +@NonNullByDefault +public class ConfigurationException extends Exception { + private static final long serialVersionUID = -803416460838670618L; + + public ConfigurationException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/PortalHandler.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/PortalHandler.java new file mode 100644 index 000000000..a51684490 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/PortalHandler.java @@ -0,0 +1,229 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ScheduledFuture; + +import javax.ws.rs.core.MediaType; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.openhab.binding.semsportal.internal.dto.BaseResponse; +import org.openhab.binding.semsportal.internal.dto.LoginRequest; +import org.openhab.binding.semsportal.internal.dto.LoginResponse; +import org.openhab.binding.semsportal.internal.dto.SEMSToken; +import org.openhab.binding.semsportal.internal.dto.Station; +import org.openhab.binding.semsportal.internal.dto.StationListRequest; +import org.openhab.binding.semsportal.internal.dto.StationListResponse; +import org.openhab.binding.semsportal.internal.dto.StationStatus; +import org.openhab.binding.semsportal.internal.dto.StatusRequest; +import org.openhab.binding.semsportal.internal.dto.StatusResponse; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * The {@link PortalHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Iwan Bron - Initial contribution + */ +@NonNullByDefault +public class PortalHandler extends BaseBridgeHandler { + private Logger logger = LoggerFactory.getLogger(PortalHandler.class); + // the settings that are needed when you do not have avalid session token yet + private static final SEMSToken SESSIONLESS_TOKEN = new SEMSToken("v2.1.0", "ios", "en"); + // the url of the SEMS portal API + private static final String BASE_URL = "https://www.semsportal.com/"; + // url for the login request, to get a valid session token + private static final String LOGIN_URL = BASE_URL + "api/v2/Common/CrossLogin"; + // url to get the status of a specific power station + private static final String STATUS_URL = BASE_URL + "api/v2/PowerStation/GetMonitorDetailByPowerstationId"; + private static final String LIST_URL = BASE_URL + "api/PowerStationMonitor/QueryPowerStationMonitorForApp"; + // the token holds the credential information for the portal + private static final String HTTP_HEADER_TOKEN = "Token"; + + // used to parse json from / to the SEMS portal API + private final Gson gson; + private final HttpClient httpClient; + + // configuration as provided by the openhab framework: initialize with defaults to prevent compiler check errors + private SEMSPortalConfiguration config = new SEMSPortalConfiguration(); + private boolean loggedIn; + private SEMSToken sessionToken = SESSIONLESS_TOKEN;// gets the default, it is needed for the login + private @Nullable StationStatus currentStatus; + + private @Nullable ScheduledFuture pollingJob; + + public PortalHandler(Bridge bridge, HttpClientFactory httpClientFactory) { + super(bridge); + httpClient = httpClientFactory.getCommonHttpClient(); + gson = new GsonBuilder().setDateFormat(SEMSPortalBindingConstants.DATE_FORMAT).create(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("No supported commands. Ignoring command {} for channel {}", command, channelUID); + return; + } + + @Override + public void initialize() { + config = getConfigAs(SEMSPortalConfiguration.class); + updateStatus(ThingStatus.UNKNOWN); + + scheduler.execute(() -> { + try { + login(); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "Error when loggin in. Check your username and password"); + } + }); + } + + @Override + public void dispose() { + ScheduledFuture localPollingJob = pollingJob; + if (localPollingJob != null) { + localPollingJob.cancel(true); + } + super.dispose(); + } + + private void login() { + loggedIn = false; + String payload = gson.toJson(new LoginRequest(config.username, config.password)); + String response = sendPost(LOGIN_URL, payload); + if (response == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Invalid response from SEMS portal"); + return; + } + LoginResponse loginResponse = gson.fromJson(response, LoginResponse.class); + if (loginResponse == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check username / password"); + return; + } + if (loginResponse.isOk()) { + logger.debug("Successfuly logged in to SEMS portal"); + if (loginResponse.getToken() != null) { + sessionToken = loginResponse.getToken(); + } + loggedIn = true; + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.CONFIGURATION_ERROR, "Check username / password"); + } + } + + private @Nullable String sendPost(String url, String payload) { + try { + Request request = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .header(HTTP_HEADER_TOKEN, gson.toJson(sessionToken)) + .content(new StringContentProvider(payload, StandardCharsets.UTF_8.name()), + MediaType.APPLICATION_JSON); + request.getHeaders().remove(HttpHeader.ACCEPT_ENCODING); + ContentResponse response = request.send(); + logger.trace("received response: {}", response.getContentAsString()); + return response.getContentAsString(); + } catch (Exception e) { + logger.debug("{} when posting to url {}", e.getClass().getSimpleName(), url, e); + } + return null; + } + + public boolean isLoggedIn() { + return loggedIn; + } + + public @Nullable StationStatus getStationStatus(String stationUUID) + throws CommunicationException, ConfigurationException { + if (!loggedIn) { + logger.debug("Not logged in. Not updating."); + return null; + } + String response = sendPost(STATUS_URL, gson.toJson(new StatusRequest(stationUUID))); + if (response == null) { + throw new CommunicationException("No response received from portal"); + } + BaseResponse semsResponse = gson.fromJson(response, BaseResponse.class); + if (semsResponse == null) { + throw new CommunicationException("Portal reponse not understood"); + } + if (semsResponse.isOk()) { + StatusResponse statusResponse = gson.fromJson(response, StatusResponse.class); + if (statusResponse == null) { + throw new CommunicationException("Portal reponse not understood"); + } + currentStatus = statusResponse.getStatus(); + updateStatus(ThingStatus.ONLINE); // we got a valid response, register as online + return currentStatus; + } else if (semsResponse.isSessionInvalid()) { + logger.debug("Session is invalidated. Attempting new login."); + login(); + return getStationStatus(stationUUID); + } else if (semsResponse.isError()) { + throw new ConfigurationException( + "ERROR status code received from SEMS portal. Please check your station ID"); + } else { + throw new CommunicationException(String.format("Unknown status code received from SEMS portal: %s - %s", + semsResponse.getCode(), semsResponse.getMsg())); + } + } + + public long getUpdateInterval() { + return config.update; + } + + public List getAllStations() { + String payload = gson.toJson(new StationListRequest()); + String response = sendPost(LIST_URL, payload); + if (response == null) { + logger.debug("Received empty list stations response from SEMS portal"); + return Collections.emptyList(); + } + StationListResponse listResponse = gson.fromJson(response, StationListResponse.class); + if (listResponse == null) { + logger.debug("Unable to read list stations response from SEMS portal"); + return Collections.emptyList(); + } + if (listResponse.isOk()) { + logger.debug("Received list of {} stations from SEMS portal", listResponse.getStations().size()); + loggedIn = true; + updateStatus(ThingStatus.ONLINE); + return listResponse.getStations(); + } else { + logger.debug("Received error response with code {} and message {} from SEMS portal", listResponse.getCode(), + listResponse.getMsg()); + return Collections.emptyList(); + } + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalBindingConstants.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalBindingConstants.java new file mode 100644 index 000000000..d1170e507 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalBindingConstants.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link SEMSPortalBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Iwan Bron - Initial contribution + */ +@NonNullByDefault +public class SEMSPortalBindingConstants { + + private static final String BINDING_ID = "semsportal"; + static final String DATE_FORMAT = "MM.dd.yyyy HH:mm:ss"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_PORTAL = new ThingTypeUID(BINDING_ID, "portal"); + public static final ThingTypeUID THING_TYPE_STATION = new ThingTypeUID(BINDING_ID, "station"); + + // the default update interval for statusses at the portal + public static final int DEFAULT_UPDATE_INTERVAL_MINUTES = 5; + + // station properties + public static final String STATION_UUID = "stationUUID"; + public static final String STATION_NAME = "stationName"; + public static final String STATION_CAPACITY = "stationCapacity"; + public static final String STATION_REPRESENTATION_PROPERTY = STATION_UUID; + public static final String STATION_LABEL_FORMAT = "Power Station %s"; + + // List of all Channel ids + public static final String CHANNEL_CURRENT_OUTPUT = "currentOutput"; + public static final String CHANNEL_LASTUPDATE = "lastUpdate"; + public static final String CHANNEL_TODAY_TOTAL = "todayTotal"; + public static final String CHANNEL_MONTH_TOTAL = "monthTotal"; + public static final String CHANNEL_OVERALL_TOTAL = "overallTotal"; + public static final String CHANNEL_TODAY_INCOME = "todayIncome"; + public static final String CHANNEL_TOTAL_INCOME = "totalIncome"; + + protected static final List ALL_CHANNELS = Arrays.asList(CHANNEL_LASTUPDATE, CHANNEL_CURRENT_OUTPUT, + CHANNEL_TODAY_TOTAL, CHANNEL_MONTH_TOTAL, CHANNEL_OVERALL_TOTAL, CHANNEL_TODAY_INCOME, + CHANNEL_TOTAL_INCOME); +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalConfiguration.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalConfiguration.java new file mode 100644 index 000000000..450353910 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SEMSPortalConfiguration} class contains fields mapping thing + * configuration parameters. + * + * @author Iwan Bron - Initial contribution + */ +@NonNullByDefault +public class SEMSPortalConfiguration { + + /** + * We need username and password of the SEMS portal to access the solar plant + * data. + * + * In the first version, you need to provide the station ID as well. Later we + * can discover it from the SEMS portal. + */ + public String username = ""; + public String password = ""; + public int update = SEMSPortalBindingConstants.DEFAULT_UPDATE_INTERVAL_MINUTES; +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalHandlerFactory.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalHandlerFactory.java new file mode 100644 index 000000000..cd9d7b516 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalHandlerFactory.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import static org.openhab.binding.semsportal.internal.SEMSPortalBindingConstants.*; + +import java.util.Hashtable; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.semsportal.internal.discovery.StationDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link SEMSPortalHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Iwan Bron - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.semsportal", service = ThingHandlerFactory.class) +public class SEMSPortalHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_STATION, THING_TYPE_PORTAL); + private HttpClientFactory httpClientFactory; + + @Activate + public SEMSPortalHandlerFactory(@Reference final HttpClientFactory httpClientFactory) { + this.httpClientFactory = httpClientFactory; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_PORTAL.equals(thingTypeUID)) { + PortalHandler handler = new PortalHandler((Bridge) thing, httpClientFactory); + Hashtable dictionary = new Hashtable<>(); + bundleContext.registerService(DiscoveryService.class.getName(), new StationDiscoveryService(handler), + dictionary); + return handler; + } + if (THING_TYPE_STATION.equals(thingTypeUID)) { + return new StationHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StateHelper.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StateHelper.java new file mode 100644 index 000000000..8a9ff3b2b --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StateHelper.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.semsportal.internal.dto.StationStatus; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Helper class to convert the POJOs of the SEMS portal response classes into openHAB State objects. + * + * @author Iwan Bron - Initial contribution + */ +@NonNullByDefault +public class StateHelper { + + private StateHelper() { + // hide constructor, no initialisation possible + } + + public static State getOperational(@Nullable StationStatus currentStatus) { + if (currentStatus == null) { + return UnDefType.UNDEF; + } + return OnOffType.from(currentStatus.isOperational()); + } + + public static State getLastUpdate(@Nullable StationStatus currentStatus) { + return currentStatus == null ? UnDefType.UNDEF : new DateTimeType(currentStatus.getLastUpdate()); + } + + public static State getCurrentOutput(@Nullable StationStatus status) { + if (status == null) { + return UnDefType.UNDEF; + } + if (status.getCurrentOutput() == null) { + return UnDefType.NULL; + } + return new QuantityType<>(status.getCurrentOutput(), Units.WATT); + } + + public static State getDayTotal(@Nullable StationStatus status) { + if (status == null) { + return UnDefType.UNDEF; + } + if (status.getDayTotal() == null) { + return UnDefType.NULL; + } + return new QuantityType<>(status.getDayTotal(), Units.KILOWATT_HOUR); + } + + public static State getMonthTotal(@Nullable StationStatus status) { + if (status == null) { + return UnDefType.UNDEF; + } + if (status.getMonthTotal() == null) { + return UnDefType.NULL; + } + return new QuantityType<>(status.getMonthTotal(), Units.KILOWATT_HOUR); + } + + public static State getOverallTotal(@Nullable StationStatus status) { + if (status == null) { + return UnDefType.UNDEF; + } + if (status.getOverallTotal() == null) { + return UnDefType.NULL; + } + return new QuantityType<>(status.getOverallTotal(), Units.KILOWATT_HOUR); + } + + public static State getDayIncome(@Nullable StationStatus status) { + if (status == null) { + return UnDefType.UNDEF; + } + if (status.getDayIncome() == null) { + return UnDefType.NULL; + } + return new DecimalType(status.getDayIncome()); + } + + public static State getTotalIncome(@Nullable StationStatus status) { + if (status == null) { + return UnDefType.UNDEF; + } + if (status.getTotalIncome() == null) { + return UnDefType.NULL; + } + return new DecimalType(status.getTotalIncome()); + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StationHandler.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StationHandler.java new file mode 100644 index 000000000..a2153276a --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StationHandler.java @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import static org.openhab.binding.semsportal.internal.SEMSPortalBindingConstants.*; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.semsportal.internal.dto.StationStatus; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link StationHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Iwan Bron - Initial contribution + */ +@NonNullByDefault +public class StationHandler extends BaseThingHandler { + private Logger logger = LoggerFactory.getLogger(StationHandler.class); + private static final long MAX_STATUS_AGE_MINUTES = 1; + + private @Nullable StationStatus currentStatus; + private LocalDateTime lastUpdate = LocalDateTime.MIN; + + public StationHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (isPortalOK()) { + if (command instanceof RefreshType) { + scheduler.execute(() -> { + ensureRecentStatus(); + updateChannelState(channelUID); + }); + } + } + } + + private boolean isPortalOK() { + PortalHandler portal = getPortal(); + return portal != null && portal.isLoggedIn(); + } + + private void updateChannelState(ChannelUID channelUID) { + if (!isPortalOK()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Unable to update station info. Check Bridge status for details."); + return; + } + switch (channelUID.getId()) { + case CHANNEL_CURRENT_OUTPUT: + updateState(channelUID.getId(), StateHelper.getCurrentOutput(currentStatus)); + break; + case CHANNEL_TODAY_TOTAL: + updateState(channelUID.getId(), StateHelper.getDayTotal(currentStatus)); + break; + case CHANNEL_MONTH_TOTAL: + updateState(channelUID.getId(), StateHelper.getMonthTotal(currentStatus)); + break; + case CHANNEL_OVERALL_TOTAL: + updateState(channelUID.getId(), StateHelper.getOverallTotal(currentStatus)); + break; + case CHANNEL_TODAY_INCOME: + updateState(channelUID.getId(), StateHelper.getDayIncome(currentStatus)); + break; + case CHANNEL_TOTAL_INCOME: + updateState(channelUID.getId(), StateHelper.getTotalIncome(currentStatus)); + break; + case CHANNEL_LASTUPDATE: + updateState(channelUID.getId(), StateHelper.getLastUpdate(currentStatus)); + break; + default: + logger.debug("No mapping found for channel {}", channelUID.getId()); + } + } + + private void ensureRecentStatus() { + if (lastUpdate.isBefore(LocalDateTime.now().minusMinutes(MAX_STATUS_AGE_MINUTES))) { + updateStation(); + } + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + scheduler.execute(() -> { + try { + scheduler.scheduleWithFixedDelay(() -> ensureRecentStatus(), 0, getUpdateInterval(), TimeUnit.MINUTES); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "Unable to update station info. Check Bridge status for details."); + } + }); + } + + private long getUpdateInterval() { + PortalHandler portal = getPortal(); + if (portal == null) { + return SEMSPortalBindingConstants.DEFAULT_UPDATE_INTERVAL_MINUTES; + } + return portal.getUpdateInterval(); + } + + private void updateStation() { + if (!isPortalOK()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Unable to update station info. Check Bridge status for details."); + return; + } + PortalHandler portal = getPortal(); + if (portal != null) { + try { + currentStatus = portal.getStationStatus(getStationUUID()); + StationStatus localCurrentStatus = currentStatus; + if (localCurrentStatus != null && localCurrentStatus.isOperational()) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, "Station not operational"); + } + updateAllChannels(); + } catch (CommunicationException commEx) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, commEx.getMessage()); + } catch (ConfigurationException confEx) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, confEx.getMessage()); + } + } else { + logger.debug("Unable to find portal for thing {}", getThing().getUID()); + } + } + + private String getStationUUID() { + String uuid = getThing().getProperties().get(STATION_UUID); + return uuid == null ? "" : uuid; + } + + private void updateAllChannels() { + for (String channelName : ALL_CHANNELS) { + Channel channel = thing.getChannel(channelName); + if (channel != null) { + updateChannelState(channel.getUID()); + } + } + } + + private @Nullable PortalHandler getPortal() { + Bridge bridge = getBridge(); + if (bridge != null && bridge.getHandler() != null && bridge.getHandler() instanceof PortalHandler) { + return (PortalHandler) bridge.getHandler(); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/discovery/StationDiscoveryService.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/discovery/StationDiscoveryService.java new file mode 100644 index 000000000..8f291cafb --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/discovery/StationDiscoveryService.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.discovery; + +import static org.openhab.binding.semsportal.internal.SEMSPortalBindingConstants.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.semsportal.internal.PortalHandler; +import org.openhab.binding.semsportal.internal.dto.Station; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; + +/** + * The discovery service can discover the power stations that are registered to the portal that it belongs to. It will + * find unique power stations and add them as a discovery result; + * + * @author Iwan Bron - Initial contribution + * + */ +@NonNullByDefault +public class StationDiscoveryService extends AbstractDiscoveryService { + + private static final int DISCOVERY_TIME = 10; + private PortalHandler portal; + private ThingUID bridgeUID; + + public StationDiscoveryService(PortalHandler bridgeHandler) { + super(Set.of(THING_TYPE_STATION), DISCOVERY_TIME); + this.portal = bridgeHandler; + this.bridgeUID = bridgeHandler.getThing().getUID(); + } + + @Override + protected void startScan() { + for (Station station : portal.getAllStations()) { + DiscoveryResult discovery = DiscoveryResultBuilder.create(createThingUUID(station)).withBridge(bridgeUID) + .withProperties(buildProperties(station)) + .withRepresentationProperty(STATION_REPRESENTATION_PROPERTY) + .withLabel(String.format(STATION_LABEL_FORMAT, station.getName())).withThingType(THING_TYPE_STATION) + .build(); + thingDiscovered(discovery); + } + stopScan(); + } + + private ThingUID createThingUUID(Station station) { + return new ThingUID(THING_TYPE_STATION, station.getStationId(), bridgeUID.getId()); + } + + private @Nullable Map buildProperties(Station station) { + Map properties = new HashMap<>(); + properties.put(Thing.PROPERTY_MODEL_ID, station.getType()); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, station.getSerialNumber()); + properties.put(STATION_NAME, station.getName()); + properties.put(STATION_CAPACITY, station.getCapacity()); + properties.put(STATION_UUID, station.getStationId()); + properties.put(STATION_CAPACITY, station.getCapacity()); + return properties; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/BaseResponse.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/BaseResponse.java new file mode 100644 index 000000000..77434d368 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/BaseResponse.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The base response contains the generic properties of each response from the portal. Depending on the request, the + * data component contains different information. The subclasses of the BaseResponse contain the mapping of the data + * with respect to their request context. + * + * @author Iwan Bron - Initial contribution + */ +@NonNullByDefault +public class BaseResponse { + public static final String OK = "0"; + + public static final String NO_SESSION = "100001"; + public static final String SESSION_EXPIRED = "100002"; + public static final String INVALID = "100005"; + public static final String EXCEPTION = "innerexception"; + + private @Nullable String code; + private @Nullable String msg; + + public @Nullable String getCode() { + return code; + } + + public @Nullable String getMsg() { + return msg; + } + + public boolean isOk() { + return OK.equals(code); + } + + public boolean isError() { + return EXCEPTION.equals(code); + } + + public boolean isSessionInvalid() { + return NO_SESSION.equals(code) || SESSION_EXPIRED.equals(code); + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/InverterDetails.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/InverterDetails.java new file mode 100644 index 000000000..3d2d128b0 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/InverterDetails.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import java.util.Date; + +import com.google.gson.annotations.SerializedName; + +/** + * POJO containing details about the inverter. Only a very small subset of the available properties is mapped + * + * @author Iwan Bron - Initial contribution + */ +public class InverterDetails { + @SerializedName("last_refresh_time") + private Date lastUpdate; + + public Date getLastUpdate() { + return lastUpdate; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/KeyPerformanceIndicators.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/KeyPerformanceIndicators.java new file mode 100644 index 000000000..f8aa8bc5f --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/KeyPerformanceIndicators.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * POJO for mapping the SEMS portal data response /data/kpi + * + * @author Iwan Bron - Initial contribution + * + */ +public class KeyPerformanceIndicators { + @SerializedName("pac") + private Double currentOutput; + @SerializedName("month_generation") + private Double monthPower; + @SerializedName("total_power") + private Double totalPower; + @SerializedName("day_income") + private Double dayIncome; + @SerializedName("total_income") + private Double totalIncome; + @SerializedName("yield_rate") + private Double yieldRate; + private String currency; + + public Double getCurrentOutput() { + return currentOutput; + } + + public Double getMonthPower() { + return monthPower; + } + + public Double getTotalPower() { + return totalPower; + } + + public Double getDayIncome() { + return dayIncome; + } + + public Double getTotalIncome() { + return totalIncome; + } + + public Double getYieldRate() { + return yieldRate; + } + + public String getCurrency() { + return currency; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginRequest.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginRequest.java new file mode 100644 index 000000000..67069aeeb --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginRequest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The login request to the portal. Response can be deserialized in a {@link LoginResponse} + * + * @author Iwan Bron - Initial contribution + */ + +@NonNullByDefault +public class LoginRequest { + private String account; + private String pwd; + + public LoginRequest(String account, String pwd) { + this.account = account; + this.pwd = pwd; + } + + public void setPwd(String pwd) { + this.pwd = pwd; + } + + public void setAccount(String account) { + this.account = account; + } + + public String getPwd() { + return pwd; + } + + public String getAccount() { + return account; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginResponse.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginResponse.java new file mode 100644 index 000000000..fba605def --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginResponse.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * Response to a {@link LoginRequest} to the portal. + * + * @author Iwan Bron - Initial contribution + */ +public class LoginResponse extends BaseResponse { + @SerializedName("data") + private SEMSToken token; + + public SEMSToken getToken() { + return token; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/SEMSToken.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/SEMSToken.java new file mode 100644 index 000000000..f1f23794a --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/SEMSToken.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +/** + * A token is returned in a successful {@Link LoginRequest} and is needed to authorize any subsequent requests. + * + * @author Iwan Bron - Initial contribution + */ +public class SEMSToken { + private String uid; + private long timestamp; + private String token; + private String client; + private String version; + private String language; + + public SEMSToken(String version, String client, String language) { + this.version = version; + this.client = client; + this.language = language; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getClient() { + return client; + } + + public void setClient(String client) { + this.client = client; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/Station.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/Station.java new file mode 100644 index 000000000..eebd48f8d --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/Station.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * POJO for mapping the portal data response to the {@link StatusRequest} and the {@Link StationListRequest} + * + * @author Iwan Bron - Initial contribution + * + */ +public class Station { + @SerializedName("powerstation_id") + private String stationId; + @SerializedName("stationname") + private String name; + @SerializedName("sn") + private String serialNumber; + private String type; + private Double capacity; + private int status; + @SerializedName("out_pac") + private Double currentPower; + @SerializedName("eday") + private Double dayTotal; + @SerializedName("emonth") + private Double monthTotal; + @SerializedName("etotal") + private Double overallTotal; + @SerializedName("d") + private InverterDetails details; + + public String getStationId() { + return stationId; + } + + public String getName() { + return name; + } + + public String getSerialNumber() { + return serialNumber; + } + + public String getType() { + return type; + } + + public Double getCapacity() { + return capacity; + } + + public int getStatus() { + return status; + } + + public Double getCurrentPower() { + return currentPower; + } + + public Double getDayTotal() { + return dayTotal; + } + + public Double getMonthTotal() { + return monthTotal; + } + + public Double getOverallTotal() { + return overallTotal; + } + + public InverterDetails getDetails() { + return details; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListRequest.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListRequest.java new file mode 100644 index 000000000..2ecaf6568 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListRequest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * Request to list all available power stations in an account. Answer can be deserialized in a + * {@link StationListResponse} + * + * @author Iwan Bron - Initial contribution + * + */ +@NonNullByDefault +public class StationListRequest { + // Properties are private but used by Gson to construct the request + @SerializedName("page_size") + private int pageSize = 5; + @SerializedName("page_index") + private int pageIndex = 1; + @SerializedName("order_by") + private String orderBy = ""; + @SerializedName("powerstation_status") + private String powerstationStatus = ""; + // @SerializedName("key") + // private String key = ""; + @SerializedName("powerstation_id") + private String powerstationId = ""; + @SerializedName("powerstation_type") + private String powerstationType = ""; +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListResponse.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListResponse.java new file mode 100644 index 000000000..22def1d29 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListResponse.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** + * POJO containing the response to the {@link StationListRequest} + * + * @author Iwan Bron - Initial contribution + */ +public class StationListResponse extends BaseResponse { + + @SerializedName("data") + private List stations; + + public List getStations() { + return stations; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationStatus.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationStatus.java new file mode 100644 index 000000000..f37835980 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationStatus.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** + * Facade for easy access to the SEMS portal data response. Data is distributed over different parts of the response + * object + * + * @author Iwan Bron - Initial contribution + */ +public class StationStatus { + @SerializedName("kpi") + private KeyPerformanceIndicators keyPerformanceIndicators; + @SerializedName("inverter") + private List stations; + + public Double getCurrentOutput() { + return keyPerformanceIndicators.getCurrentOutput(); + } + + public Double getDayTotal() { + return stations.isEmpty() ? null : stations.get(0).getDayTotal(); + } + + public Double getMonthTotal() { + return stations.isEmpty() ? null : stations.get(0).getMonthTotal(); + } + + public Double getOverallTotal() { + return stations.isEmpty() ? null : stations.get(0).getOverallTotal(); + } + + public Double getDayIncome() { + return keyPerformanceIndicators.getDayIncome(); + } + + public Double getTotalIncome() { + return keyPerformanceIndicators.getTotalIncome(); + } + + public boolean isOperational() { + return stations.isEmpty() ? false : stations.get(0).getStatus() == 1; + } + + public ZonedDateTime getLastUpdate() { + if (stations.isEmpty()) { + return null; + } + return ZonedDateTime.ofInstant(stations.get(0).getDetails().getLastUpdate().toInstant(), + ZoneId.systemDefault()); + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusRequest.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusRequest.java new file mode 100644 index 000000000..3173a4c11 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusRequest.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Request for the status of a Power Station. Answer can be deserialized in a {@link StatusResponse} + * + * @author Iwan Bron - Initial contribution + */ + +@NonNullByDefault +public class StatusRequest { + private String powerStationId; + + public StatusRequest(String powerStationId) { + this.powerStationId = powerStationId; + } + + public void setPowerStationId(String powerStationId) { + this.powerStationId = powerStationId; + } + + public String getPowerStationId() { + return powerStationId; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusResponse.java b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusResponse.java new file mode 100644 index 000000000..c9e29deb3 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusResponse.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * POJO containing (a small subset of) the data received from the portal when issuing a {@link StationRequest) + * + * @author Iwan Bron - Initial contribution + */ +public class StatusResponse extends BaseResponse { + + @SerializedName("data") + private StationStatus status; + + public StationStatus getStatus() { + return status; + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 000000000..d802b1afd --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,14 @@ + + + + SEMSPortal Binding + + This is the binding for SEMSPortal. The SEMS portal is where a GoodWE solar installation uploads it's + data. The SEMS portal has a lot of data, only a few of them are currently mapped to a channel. + You will need an account + at semsportal.com and have your solar installation registered on that account. + + + diff --git a/bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 000000000..0339f2f4b --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,96 @@ + + + + + + The SEMS Portal is where the data about powerstations is collected online. + Configuration will only work if + you have used this account at least once in the portal itsself. + + + GoodWe + + username + + + + + Username (email address) of the account at the SEMS portal + + + password + + Password of the SEMS Portal + + + + Number of minutes between updates. Minimum is 1 minute, maximum is 60. The default is 5 minutes. + 5 + + + + + + + + + + + A Power Station is the GoodWe converter that is connected through the internet with the SEMSPortal. + + + + + + + + + + + + + + + + DateTime + + Timestamp that the last information was received from the station. This is not the same as the last time + that was checked: the station goes offline at night. + + + Number:Power + + Current output in Watts + + + Number:Energy + + Todays total output in kWh + + + Number:Energy + + The total output of this month in kWh + + + Number:Energy + + The total output from the start of the installation in kWh + + + Number + + Todays income. Only reports if you have set the tariffs in the SEMS portal. Unit is the currency that is + set in these tariffs. + + + Number + + Total income since installation. Only reports if you have set the tariffs in the SEMS portal. Unit is the + currency that is set in these tariffs. + + + diff --git a/bundles/org.openhab.binding.semsportal/src/test/java/org/openhab/binding/semsportal/internal/SEMSJsonParserTest.java b/bundles/org.openhab.binding.semsportal/src/test/java/org/openhab/binding/semsportal/internal/SEMSJsonParserTest.java new file mode 100644 index 000000000..b9eaa2756 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/test/java/org/openhab/binding/semsportal/internal/SEMSJsonParserTest.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2021 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.semsportal.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.semsportal.internal.dto.BaseResponse; +import org.openhab.binding.semsportal.internal.dto.LoginResponse; +import org.openhab.binding.semsportal.internal.dto.StationListResponse; +import org.openhab.binding.semsportal.internal.dto.StatusResponse; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * @author Iwan Bron - Initial contribution + */ +@NonNullByDefault +public class SEMSJsonParserTest { + @Test + public void testParseSuccessStatusResult() throws Exception { + String json = Files.readString(Paths.get("src/test/resources/success_status.json")); + StatusResponse response = getGson().fromJson(json, StatusResponse.class); + assertNotNull(response, "Expected deserialized StatusResponse"); + if (response != null) {// response cannot be null, was asserted before, but code check produces a warning + assertTrue(response.isOk(), "Successresponse should be OK"); + assertNotNull(response.getStatus(), "Expected deserialized StatusResponse.status"); + assertEquals(381.0, response.getStatus().getCurrentOutput(), "Current Output parsed correctly"); + assertEquals(0.11, response.getStatus().getDayIncome(), "Day income parsed correctly"); + assertEquals(0.5, response.getStatus().getDayTotal(), "Day total parsed correctly"); + assertEquals(ZonedDateTime.of(2021, 2, 6, 11, 22, 48, 0, ZoneId.systemDefault()), + response.getStatus().getLastUpdate(), "Last update parsed correctly"); + assertEquals(17.2, response.getStatus().getMonthTotal(), "Month total parsed correctly"); + assertEquals(7379.0, response.getStatus().getOverallTotal(), "Overall total parsed correctly"); + assertEquals(823.38, response.getStatus().getTotalIncome(), "Total income parsed correctly"); + } + } + + @Test + public void testParseErrorStatusResult() throws Exception { + String json = Files.readString(Paths.get("src/test/resources/error_status.json")); + BaseResponse response = getGson().fromJson(json, BaseResponse.class); + assertNotNull(response, "Expected deserialized StatusResponse"); + if (response != null) {// response cannot be null, was asserted before, but code check produces a warning + assertEquals(response.getCode(), BaseResponse.EXCEPTION, "Error response shoud have error code"); + assertTrue(response.isError(), "Error response should have isError = true"); + } + } + + @Test + public void testParseSuccessLoginResult() throws Exception { + String json = Files.readString(Paths.get("src/test/resources/success_login.json")); + LoginResponse response = getGson().fromJson(json, LoginResponse.class); + assertNotNull(response, "Expected deserialized LoginResponse"); + if (response != null) {// response cannot be null, was asserted before, but code check produces a warning + assertTrue(response.isOk(), "Success response should result in OK"); + assertNotNull(response.getToken(), "Success response should result in token"); + } + } + + @Test + public void testParseErrorLoginResult() throws Exception { + String json = Files.readString(Paths.get("src/test/resources/error_login.json")); + LoginResponse response = getGson().fromJson(json, LoginResponse.class); + assertNotNull(response, "Expected deserialized LoginResponse"); + if (response != null) {// response cannot be null, was asserted before, but code check produces a warning + assertFalse(response.isOk(), "Error response should not result in OK"); + assertNull(response.getToken(), "Error response should have null token"); + } + } + + @Test + public void testParseSuccessListResult() throws Exception { + String json = Files.readString(Paths.get("src/test/resources/success_list.json")); + StationListResponse response = getGson().fromJson(json, StationListResponse.class); + assertNotNull(response, "Expected deserialized StationListResponse"); + if (response != null) {// response cannot be null, was asserted before, but code check produces a warning + assertTrue(response.isOk(), "Success response should result in OK"); + assertNotNull(response.getStations(), "List response should have station list"); + assertEquals(1, response.getStations().size(), "List response should have station list"); + } + } + + private Gson getGson() { + return new GsonBuilder().setDateFormat(SEMSPortalBindingConstants.DATE_FORMAT).create(); + } +} diff --git a/bundles/org.openhab.binding.semsportal/src/test/resources/error_login.json b/bundles/org.openhab.binding.semsportal/src/test/resources/error_login.json new file mode 100644 index 000000000..e65e67ef8 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/test/resources/error_login.json @@ -0,0 +1,13 @@ +{ + "hasError": false, + "code": 100005, + "msg": "Email or password error.", + "data": null, + "components": { + "para": null, + "langVer": 97, + "timeSpan": 0, + "api": "http://www.semsportal.com:82/api/v2/Common/CrossLogin", + "msgSocketAdr": "https://eu-xxzx.semsportal.com" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.semsportal/src/test/resources/error_status.json b/bundles/org.openhab.binding.semsportal/src/test/resources/error_status.json new file mode 100644 index 000000000..03b7bd5e2 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/test/resources/error_status.json @@ -0,0 +1,17 @@ +{ + "language": "en", + "function": [ + "ADD" + ], + "hasError": true, + "msg": "未将对象引用设置到对象的实例。", + "code": "innerexception", + "data": "", + "components": { + "para": "{\"model\":{\"PowerStationId\":\"ERRORCODE\"}}", + "langVer": 97, + "timeSpan": 8, + "api": "http://www.semsportal.com:82/api/v2/PowerStation/GetMonitorDetailByPowerstationId", + "msgSocketAdr": null + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.semsportal/src/test/resources/success_list.json b/bundles/org.openhab.binding.semsportal/src/test/resources/success_list.json new file mode 100644 index 000000000..d9feee90e --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/test/resources/success_list.json @@ -0,0 +1,73 @@ +{ + "hasError": false, + "code": 0, + "msg": "Success", + "data": [ + { + "powerstation_id": "000000-0000000-0000000-00000", + "stationname": "place", + "first_letter": "", + "adcode": "11111111111111", + "location": "", + "status": -1, + "pac": 0.0, + "capacity": 4.5, + "eday": 13.1, + "emonth": 195.0, + "eday_income": 604.318, + "etotal": 2746.9, + "powerstation_type": "residential", + "pre_org_id": null, + "org_id": null, + "longitude": "16", + "latitude": "23", + "pac_kw": 2746.9, + "to_hour": 2.911111111111111, + "weather": { + "HeWeather6": [ + { + "basic": { + "cid": "XXXXXXXX", + "location": "Location", + "parent_city": "City", + "admin_area": "Area", + "cnty": "Country", + "lat": "16", + "lon": "23", + "tz": "+1.00" + }, + "update": { + "loc": "2019-08-12 23:57", + "utc": "2019-08-12 22:57" + }, + "status": "ok", + "now": { + "cloud": "100", + "cond_code": "300", + "cond_txt": "Shower Rain", + "fl": "14", + "hum": "100", + "pcpn": "0.3", + "pres": "1014", + "tmp": "14", + "vis": "16", + "wind_deg": "90", + "wind_dir": "E", + "wind_sc": "1", + "wind_spd": "4" + } + } + ] + }, + "currency": "EUR", + "yield_rate": 0.22, + "is_stored": false + } + ], + "components": { + "para": null, + "langVer": 40, + "timeSpan": 0, + "api": "http://eu.semsportal.com:82/api/PowerStationMonitor/QueryPowerStationMonitorForApp" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.semsportal/src/test/resources/success_login.json b/bundles/org.openhab.binding.semsportal/src/test/resources/success_login.json new file mode 100644 index 000000000..3c585960a --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/test/resources/success_login.json @@ -0,0 +1,21 @@ +{ + "hasError": false, + "code": 0, + "msg": "Success", + "data": { + "uid": "0000000-0000-0000-00000000", + "timestamp": 1612644477008, + "token": "12345678", + "client": "ios", + "version": "v2.1.0", + "language": "en" + }, + "components": { + "para": null, + "langVer": 97, + "timeSpan": 0, + "api": "http://eu.semsportal.com:82/api/Auth/GetTokenV2", + "msgSocketAdr": "https://eu-xxzx.semsportal.com" + }, + "api": "https://eu.semsportal.com/api/" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.semsportal/src/test/resources/success_status.json b/bundles/org.openhab.binding.semsportal/src/test/resources/success_status.json new file mode 100644 index 000000000..f95b48f66 --- /dev/null +++ b/bundles/org.openhab.binding.semsportal/src/test/resources/success_status.json @@ -0,0 +1,941 @@ +{ + "language": "en", + "function": [ + "ADD", + "VIEW", + "EDIT", + "DELETE", + "INVERTER_A", + "INVERTER_E", + "INVERTER_D" + ], + "hasError": false, + "msg": "success", + "code": "0", + "data": { + "info": { + "powerstation_id": "RANDOM_UUID", + "time": "02/06/2021 11:24:41", + "date_format": "MM.dd.yyyy", + "date_format_ym": "MM.yyyy", + "stationname": "Home", + "address": "Somewhere", + "owner_name": null, + "owner_phone": null, + "owner_email": "some@mailaddress.com", + "battery_capacity": 0.0, + "turnon_time": "04/06/2020 18:19:40", + "create_time": "04/06/2020 18:18:32", + "capacity": 7.8, + "longitude": 16, + "latitude": 23, + "powerstation_type": "Residential", + "status": 1, + "is_stored": false, + "is_powerflow": false, + "charts_type": 4, + "has_pv": true, + "has_statistics_charts": false, + "only_bps": false, + "only_bpu": false, + "time_span": -1.0, + "pr_value": "" + }, + "kpi": { + "month_generation": 17.7, + "pac": 381.0, + "power": 0.5, + "total_power": 7379.0, + "day_income": 0.11, + "total_income": 823.38, + "yield_rate": 0.12, + "currency": "EUR" + }, + "images": [], + "weather": { + "HeWeather6": [ + { + "daily_forecast": [ + { + "cond_code_d": "407", + "cond_code_n": "499", + "cond_txt_d": "Snow Flurry", + "cond_txt_n": "Snow", + "date": "2021-02-06", + "time": "2021-02-06 00:00:00", + "hum": "79", + "pcpn": "0.2", + "pop": "49", + "pres": "1011", + "tmp_max": "0", + "tmp_min": "-5", + "uv_index": "0", + "vis": "6", + "wind_deg": "84", + "wind_dir": "E", + "wind_sc": "4-5", + "wind_spd": "34" + }, + { + "cond_code_d": "499", + "cond_code_n": "499", + "cond_txt_d": "Snow", + "cond_txt_n": "Snow", + "date": "2021-02-07", + "time": "2021-02-07 00:00:00", + "hum": "93", + "pcpn": "7.2", + "pop": "82", + "pres": "1006", + "tmp_max": "-3", + "tmp_min": "-6", + "uv_index": "0", + "vis": "1", + "wind_deg": "70", + "wind_dir": "NE", + "wind_sc": "6-7", + "wind_spd": "46" + }, + { + "cond_code_d": "499", + "cond_code_n": "101", + "cond_txt_d": "Snow", + "cond_txt_n": "Cloudy", + "date": "2021-02-08", + "time": "2021-02-08 00:00:00", + "hum": "94", + "pcpn": "1.1", + "pop": "55", + "pres": "1005", + "tmp_max": "-3", + "tmp_min": "-7", + "uv_index": "0", + "vis": "1", + "wind_deg": "84", + "wind_dir": "E", + "wind_sc": "3-4", + "wind_spd": "20" + }, + { + "cond_code_d": "901", + "cond_code_n": "901", + "cond_txt_d": "Cold", + "cond_txt_n": "Cold", + "date": "2021-02-09", + "time": "2021-02-09 00:00:00", + "hum": "92", + "pcpn": "0.0", + "pop": "25", + "pres": "1009", + "tmp_max": "-4", + "tmp_min": "-9", + "uv_index": "1", + "vis": "24", + "wind_deg": "85", + "wind_dir": "E", + "wind_sc": "3-4", + "wind_spd": "18" + }, + { + "cond_code_d": "901", + "cond_code_n": "103", + "cond_txt_d": "Cold", + "cond_txt_n": "Partly Cloudy", + "date": "2021-02-10", + "time": "2021-02-10 00:00:00", + "hum": "90", + "pcpn": "0.0", + "pop": "12", + "pres": "1019", + "tmp_max": "-3", + "tmp_min": "-7", + "uv_index": "1", + "vis": "25", + "wind_deg": "67", + "wind_dir": "NE", + "wind_sc": "3-4", + "wind_spd": "14" + }, + { + "cond_code_d": "103", + "cond_code_n": "101", + "cond_txt_d": "Partly Cloudy", + "cond_txt_n": "Cloudy", + "date": "2021-02-11", + "time": "2021-02-11 00:00:00", + "hum": "93", + "pcpn": "0.0", + "pop": "8", + "pres": "1018", + "tmp_max": "-1", + "tmp_min": "-7", + "uv_index": "2", + "vis": "6", + "wind_deg": "187", + "wind_dir": "S", + "wind_sc": "1-2", + "wind_spd": "11" + }, + { + "cond_code_d": "999", + "cond_code_n": "999", + "cond_txt_d": "-", + "cond_txt_n": "-", + "date": "2021-02-12", + "time": "2021-02-12 00:00:00", + "hum": "-", + "pcpn": "-", + "pop": "-", + "pres": "-", + "tmp_max": "-", + "tmp_min": "-", + "uv_index": "-", + "vis": "-", + "wind_deg": "-", + "wind_dir": "--", + "wind_sc": "--", + "wind_spd": "-" + } + ], + "basic": { + "cid": "NL2755251", + "location": "SomeCity", + "cnty": "SomeCountry", + "lat": "13", + "lon": "26", + "tz": "+5.00" + }, + "update": { + "loc": "2021-02-05 23:56:00", + "utc": "2021-02-05 22:56:00" + }, + "status": "ok" + } + ] + }, + "inverter": [ + { + "sn": "56000222200W0000", + "dict": { + "left": [ + { + "isHT": false, + "key": "dmDeviceType", + "value": "GW6000-DT", + "unit": "", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "serialNum", + "value": "5600000000000000", + "unit": "", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "laCheckcode", + "value": "026848", + "unit": "", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "capacity", + "value": "6", + "unit": "kW", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "laConnected", + "value": "04.06.2020 18:19:40", + "unit": "", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "InverterPowerOfPlantMonitor", + "value": "0.381", + "unit": "kW", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "outputV", + "value": "233.1/233.5/228.7", + "unit": "V", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "acCurrent", + "value": "0.5/0.4/0.5", + "unit": "A", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "acFrequency", + "value": "50.00/50.00/50.00", + "unit": "Hz", + "isFaultMsg": 0, + "faultMsgCode": 0 + } + ], + "right": [ + { + "isHT": false, + "key": "innerTemp", + "value": "32.6", + "unit": "℃", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "dcVandC1", + "value": "720.4/0.4", + "unit": "V/A", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "dcVandC2", + "value": "0.0/0.0", + "unit": "V/A", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "dcVandC3", + "value": "--", + "unit": "V/A", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "dcVandC4", + "value": "--", + "unit": "V/A", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "strCurrent1", + "value": "--", + "unit": "A", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "strCurrent2", + "value": "--", + "unit": "A", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "strCurrent3", + "value": "--", + "unit": "A", + "isFaultMsg": 0, + "faultMsgCode": 0 + }, + { + "isHT": false, + "key": "strCurrent4", + "value": "--", + "unit": "A", + "isFaultMsg": 0, + "faultMsgCode": 0 + } + ] + }, + "is_stored": false, + "name": "home", + "in_pac": 6554.3, + "out_pac": 381.0, + "eday": 0.5, + "emonth": 17.2, + "etotal": 7379.0, + "status": 1, + "turnon_time": "04/06/2020 18:19:40", + "releation_id": "5600000000000000", + "type": "GW6000-DT", + "capacity": 6.0, + "d": { + "pw_id": "5600000000000000", + "capacity": "6kW", + "model": "GW6000-DT", + "output_power": "381W", + "output_current": "0.5A", + "grid_voltage": "233.1V", + "backup_output": "4294967.295V/0W", + "soc": "400%", + "soh": "0%", + "last_refresh_time": "02.06.2021 11:22:48", + "work_mode": "Wait Mode", + "dc_input1": "720.4V/0.4A", + "dc_input2": "0V/0A", + "battery": "65535V/6553.5A/429483622W", + "bms_status": "DischargingOfBattery", + "warning": "Over Temperature/Under Temperature/Cell Voltage Differences/Over Total Voltage/Discharge Over Current/Charge Over Current/Under SOC/Under Total Voltage/Communication Fail/Output Short/SOC Too High/BMS Module Fault/BMS System Fault/BMS Internal Fault/TBD/TBD", + "charge_current_limit": "1A", + "discharge_current_limit": "0A", + "firmware_version": 181809.0, + "creationDate": "02/06/2021 18:22:48", + "eDay": 0.5, + "eTotal": 7379.0, + "pac": 381.0, + "hTotal": 5132.0, + "vpv1": 720.4, + "vpv2": 0.0, + "vpv3": 6553.5, + "vpv4": 6553.5, + "ipv1": 0.4, + "ipv2": 0.0, + "ipv3": 6553.5, + "ipv4": 6553.5, + "vac1": 233.1, + "vac2": 233.5, + "vac3": 228.7, + "iac1": 0.5, + "iac2": 0.4, + "iac3": 0.5, + "fac1": 50.0, + "fac2": 50.0, + "fac3": 50.0, + "istr1": 0.0, + "istr2": 0.0, + "istr3": 0.0, + "istr4": 0.0, + "istr5": 0.0, + "istr6": 0.0, + "istr7": 0.0, + "istr8": 0.0, + "istr9": 0.0, + "istr10": 0.0, + "istr11": 0.0, + "istr12": 0.0, + "istr13": 0.0, + "istr14": 0.0, + "istr15": 0.0, + "istr16": 0.0 + }, + "it_change_flag": false, + "tempperature": 32.6, + "check_code": "026848", + "next": null, + "prev": null, + "next_device": { + "sn": null, + "isStored": false + }, + "prev_device": { + "sn": null, + "isStored": false + }, + "invert_full": { + "sn": "5600000000000000", + "powerstation_id": "5600000000000000", + "name": "home", + "model_type": "GW6000-DT", + "change_type": 0, + "change_time": 0, + "capacity": 6.0, + "eday": 0.5, + "iday": 0.11, + "etotal": 7379.0, + "itotal": 1623.38, + "hour_total": 5132.0, + "status": 1, + "turnon_time": 1586168380200, + "pac": 381.0, + "tempperature": 32.6, + "vpv1": 720.4, + "vpv2": 0.0, + "vpv3": 6553.5, + "vpv4": 6553.5, + "ipv1": 0.4, + "ipv2": 0.0, + "ipv3": 6553.5, + "ipv4": 6553.5, + "vac1": 233.1, + "vac2": 233.5, + "vac3": 228.7, + "iac1": 0.5, + "iac2": 0.4, + "iac3": 0.5, + "fac1": 50.0, + "fac2": 50.0, + "fac3": 50.0, + "istr1": 0.0, + "istr2": 0.0, + "istr3": 0.0, + "istr4": 0.0, + "istr5": 0.0, + "istr6": 0.0, + "istr7": 0.0, + "istr8": 0.0, + "istr9": 0.0, + "istr10": 0.0, + "istr11": 0.0, + "istr12": 0.0, + "istr13": 0.0, + "istr14": 0.0, + "istr15": 0.0, + "istr16": 0.0, + "last_time": 1612606968847, + "vbattery1": 65535.0, + "ibattery1": 6553.5, + "pmeter": 381.0, + "soc": 400.0, + "soh": -0.100000000000364, + "bms_discharge_i_max": null, + "bms_charge_i_max": 1.0, + "bms_warning": 0, + "bms_alarm": 65535, + "battary_work_mode": 2, + "workmode": 1, + "vload": 4294967.295, + "iload": 4294901.76, + "firmwareversion": 1818.0, + "pbackup": 0.0, + "seller": 0.0, + "buy": 0.0, + "yesterdaybuytotal": null, + "yesterdaysellertotal": null, + "yesterdayct2sellertotal": null, + "yesterdayetotal": null, + "yesterdayetotalload": null, + "thismonthetotle": 17.2, + "lastmonthetotle": 7361.3, + "ram": 9.0, + "outputpower": 381.0, + "fault_messge": 0, + "isbuettey": false, + "isbuetteybps": false, + "isbuetteybpu": false, + "isESUOREMU": false, + "backUpPload_S": 0.0, + "backUpVload_S": 0.0, + "backUpIload_S": 0.0, + "backUpPload_T": 0.0, + "backUpVload_T": 0.0, + "backUpIload_T": 0.0, + "eTotalBuy": null, + "eDayBuy": null, + "eBatteryCharge": null, + "eChargeDay": null, + "eBatteryDischarge": null, + "eDischargeDay": null, + "battStrings": 6553.5, + "meterConnectStatus": null, + "mtActivepowerR": 0.0, + "mtActivepowerS": 0.0, + "mtActivepowerT": 0.0, + "ezPro_connect_status": null, + "dataloggersn": "", + "equipment_name": null, + "hasmeter": false, + "meter_type": null, + "pre_hour_lasttotal": null, + "pre_hour_time": null, + "current_hour_pv": 0.0, + "extend_properties": null, + "eP_connect_status_happen": null, + "eP_connect_status_recover": null + }, + "time": "02/06/2021 11:24:41", + "battery": "65535V/6553.5A/429483622W", + "firmware_version": 181809.0, + "warning_bms": "Over Temperature/Under Temperature/Cell Voltage Differences/Over Total Voltage/Discharge Over Current/Charge Over Current/Under SOC/Under Total Voltage/Communication Fail/Output Short/SOC Too High/BMS Module Fault/BMS System Fault/BMS Internal Fault/TBD/TBD", + "soh": "0%", + "discharge_current_limit_bms": "0A", + "charge_current_limit_bms": "1A", + "soc": "400%", + "pv_input_2": "0V/0A", + "pv_input_1": "720.4V/0.4A", + "back_up_output": "4294967.295V/0W", + "output_voltage": "233.1V", + "backup_voltage": "4294967.295V", + "output_current": "0.5A", + "output_power": "381W", + "total_generation": "7379kWh", + "daily_generation": "0.5kWh", + "battery_charging": "65535V/6553.5A/429483622W", + "last_refresh_time": "02/06/2021 11:22:48", + "bms_status": "DischargingOfBattery", + "pw_id": "0000000-0000-0000-00000000", + "fault_message": "", + "battery_power": 429483622.5, + "point_index": "3", + "points": [ + { + "target_index": 1, + "target_name": "Vpv1", + "display": "Vpv1(V)", + "unit": "V", + "target_key": "Vpv1", + "text_cn": "直流电压1", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 2, + "target_name": "Vpv2", + "display": "Vpv2(V)", + "unit": "V", + "target_key": "Vpv2", + "text_cn": "直流电压2", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 5, + "target_name": "Ipv1", + "display": "Ipv1(A)", + "unit": "A", + "target_key": "Ipv1", + "text_cn": "直流电流1", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 6, + "target_name": "Ipv2", + "display": "Ipv2(A)", + "unit": "A", + "target_key": "Ipv2", + "text_cn": "直流电流2", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 9, + "target_name": "Vac1", + "display": "Vac1(V)", + "unit": "V", + "target_key": "Vac1", + "text_cn": "交流电压1", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 10, + "target_name": "Vac2", + "display": "Vac2(V)", + "unit": "V", + "target_key": "Vac2", + "text_cn": "交流电压2", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 11, + "target_name": "Vac3", + "display": "Vac3(V)", + "unit": "V", + "target_key": "Vac3", + "text_cn": "交流电压3", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 12, + "target_name": "Iac1", + "display": "Iac1(A)", + "unit": "A", + "target_key": "Iac1", + "text_cn": "交流电流1", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 13, + "target_name": "Iac2", + "display": "Iac2(A)", + "unit": "A", + "target_key": "Iac2", + "text_cn": "交流电流2", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 14, + "target_name": "Iac3", + "display": "Iac3(A)", + "unit": "A", + "target_key": "Iac3", + "text_cn": "交流电流3", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 15, + "target_name": "Fac1", + "display": "Fac1(Hz)", + "unit": "Hz", + "target_key": "Fac1", + "text_cn": "频率1", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 16, + "target_name": "Fac2", + "display": "Fac2(Hz)", + "unit": "Hz", + "target_key": "Fac2", + "text_cn": "频率2", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 17, + "target_name": "Fac3", + "display": "Fac3(Hz)", + "unit": "Hz", + "target_key": "Fac3", + "text_cn": "频率3", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 18, + "target_name": "Pac", + "display": "Pac(W)", + "unit": "W", + "target_key": "Pac", + "text_cn": "功率", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 19, + "target_name": "WorkMode", + "display": "WorkMode()", + "unit": "", + "target_key": "WorkMode", + "text_cn": "工作模式", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 20, + "target_name": "Temperature", + "display": "Temperature(℃)", + "unit": "℃", + "target_key": "Tempperature", + "text_cn": "温度", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 21, + "target_name": "Daily Generation", + "display": "Daily Generation(kWh)", + "unit": "kWh", + "target_key": "EDay", + "text_cn": "日发电量", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 22, + "target_name": "Total Generation", + "display": "Total Generation(kWh)", + "unit": "kWh", + "target_key": "ETotal", + "text_cn": "总发电量", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 23, + "target_name": "HTotal", + "display": "HTotal(h)", + "unit": "h", + "target_key": "HTotal", + "text_cn": "工作时长", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + }, + { + "target_index": 36, + "target_name": "RSSI", + "display": "RSSI(%)", + "unit": "%", + "target_key": "Reserved5", + "text_cn": "GPRS信号强度", + "target_sn_six": null, + "target_sn_seven": null, + "target_type": null, + "storage_name": null + } + ], + "backup_pload_s": 0.0, + "backup_vload_s": 0.0, + "backup_iload_s": 0.0, + "backup_pload_t": 0.0, + "backup_vload_t": 0.0, + "backup_iload_t": 0.0, + "etotal_buy": null, + "eday_buy": null, + "ebattery_charge": null, + "echarge_day": null, + "ebattery_discharge": null, + "edischarge_day": null, + "batt_strings": 6553.5, + "meter_connect_status": null, + "mtactivepower_r": 0.0, + "mtactivepower_s": 0.0, + "mtactivepower_t": 0.0, + "has_tigo": false, + "canStartIV": false + } + ], + "hjgx": { + "co2": 7.3568630000000006, + "tree": 403.26234999999997, + "coal": 2.981116 + }, + "pre_powerstation_id": null, + "nex_powerstation_id": null, + "homKit": { + "homeKitLimit": false, + "sn": null + }, + "isTigo": false, + "smuggleInfo": { + "isAllSmuggle": false, + "isSmuggle": false, + "descriptionText": null, + "sns": null + }, + "hasPowerflow": false, + "powerflow": null, + "hasEnergeStatisticsCharts": false, + "energeStatisticsCharts": { + "contributingRate": 1.0, + "selfUseRate": 1.0, + "sum": 0.5, + "buy": 0.0, + "buyPercent": 0.0, + "sell": 0.0, + "sellPercent": 0.0, + "selfUseOfPv": 0.5, + "consumptionOfLoad": 0.5, + "chartsType": 4, + "hasPv": true, + "hasCharge": false, + "charge": 0.0, + "disCharge": 0.0 + }, + "energeStatisticsTotals": { + "contributingRate": 1.0, + "selfUseRate": 1.0, + "sum": 157.2, + "buy": 0.0, + "buyPercent": 0.0, + "sell": 0.0, + "sellPercent": 0.0, + "selfUseOfPv": 157.2, + "consumptionOfLoad": 157.2, + "chartsType": 4, + "hasPv": true, + "hasCharge": false, + "charge": 0.0, + "disCharge": 0.0 + }, + "soc": { + "power": 0, + "status": -1 + }, + "environmental": [], + "equipment": [ + { + "type": "5", + "title": "home", + "status": 1, + "statusText": null, + "capacity": null, + "actionThreshold": null, + "subordinateEquipment": "", + "powerGeneration": "Power:0.381(kW)", + "eday": "Today Generation: 0.5(kWh)", + "brand": "", + "isStored": false, + "soc": "SOC:400%", + "isChange": false, + "relationId": "0000000-0000-0000-00000000", + "sn": "5600000000000000", + "has_tigo": false, + "is_sec": false, + "is_secs": false, + "targetPF": null, + "exportPowerlimit": null + } + ] + }, + "components": { + "para": "{\"model\":{\"PowerStationId\":\"0000000-0000-0000-00000000\"}}", + "langVer": 97, + "timeSpan": 177, + "api": "http://www.semsportal.com:82/api/v2/PowerStation/GetMonitorDetailByPowerstationId", + "msgSocketAdr": null + } +} \ No newline at end of file diff --git a/bundles/pom.xml b/bundles/pom.xml index 55732ad1a..2205dbaa7 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -281,6 +281,7 @@ org.openhab.binding.sagercaster org.openhab.binding.samsungtv org.openhab.binding.satel + org.openhab.binding.semsportal org.openhab.binding.senechome org.openhab.binding.seneye org.openhab.binding.sensebox