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