]> git.basschouten.com Git - openhab-addons.git/commitdiff
[semsportal] initial contribution (#10100)
authorIwan Bron <44524316+itb3@users.noreply.github.com>
Sun, 23 May 2021 18:45:04 +0000 (20:45 +0200)
committerGitHub <noreply@github.com>
Sun, 23 May 2021 18:45:04 +0000 (20:45 +0200)
Signed-off-by: Iwan Bron <bron@olisa.eu>
36 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.semsportal/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/README.md [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/CommunicationException.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/ConfigurationException.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/PortalHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/SEMSPortalHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StateHelper.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/StationHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/discovery/StationDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/BaseResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/InverterDetails.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/KeyPerformanceIndicators.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/LoginResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/SEMSToken.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/Station.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationListResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StationStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/java/org/openhab/binding/semsportal/internal/dto/StatusResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/test/java/org/openhab/binding/semsportal/internal/SEMSJsonParserTest.java [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/test/resources/error_login.json [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/test/resources/error_status.json [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/test/resources/success_list.json [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/test/resources/success_login.json [new file with mode: 0644]
bundles/org.openhab.binding.semsportal/src/test/resources/success_status.json [new file with mode: 0644]
bundles/pom.xml

index ce4bd7ea6d42ccb663266cb408d4fd32550cad5b..65619f4bc71c9878125d8de5e09abed7b20c7a78 100644 (file)
 /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
index be4084107341b0bde2aabd460a8745112b83e6fa..a3396a3a3ddf660d6b247b68871632c193c69d07 100644 (file)
       <artifactId>org.openhab.binding.satel</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.semsportal</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.senechome</artifactId>
diff --git a/bundles/org.openhab.binding.semsportal/NOTICE b/bundles/org.openhab.binding.semsportal/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..1d6929a
--- /dev/null
@@ -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 (file)
index 0000000..2846338
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.semsportal</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: SEMSPortal Binding</name>
+
+</project>
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 (file)
index 0000000..8e6e2ea
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.semsportal-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-semsportal" description="SEMSPortal Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.semsportal/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..4b56629
--- /dev/null
@@ -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 (file)
index 0000000..3111157
--- /dev/null
@@ -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 (file)
index 0000000..a516844
--- /dev/null
@@ -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<Station> 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 (file)
index 0000000..d1170e5
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..4503539
--- /dev/null
@@ -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 (file)
index 0000000..cd9d7b5
--- /dev/null
@@ -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<ThingTypeUID> 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<String, Object> 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 (file)
index 0000000..8a9ff3b
--- /dev/null
@@ -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 (file)
index 0000000..a215327
--- /dev/null
@@ -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 (file)
index 0000000..8f291ca
--- /dev/null
@@ -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<String, Object> buildProperties(Station station) {
+        Map<String, Object> 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 (file)
index 0000000..77434d3
--- /dev/null
@@ -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 (file)
index 0000000..3d2d128
--- /dev/null
@@ -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 (file)
index 0000000..f8aa8bc
--- /dev/null
@@ -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 (file)
index 0000000..67069ae
--- /dev/null
@@ -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 (file)
index 0000000..fba605d
--- /dev/null
@@ -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 (file)
index 0000000..f1f2379
--- /dev/null
@@ -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 (file)
index 0000000..eebd48f
--- /dev/null
@@ -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 (file)
index 0000000..2ecaf65
--- /dev/null
@@ -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 (file)
index 0000000..22def1d
--- /dev/null
@@ -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<Station> stations;
+
+    public List<Station> 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 (file)
index 0000000..f378359
--- /dev/null
@@ -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<Station> 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 (file)
index 0000000..3173a4c
--- /dev/null
@@ -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 (file)
index 0000000..c9e29de
--- /dev/null
@@ -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 (file)
index 0000000..d802b1a
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="semsportal" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>SEMSPortal Binding</name>
+       <description>
+               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.
+       </description>
+
+</binding:binding>
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 (file)
index 0000000..0339f2f
--- /dev/null
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="semsportal"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="portal">
+               <label>SEMS Portal</label>
+               <description>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.</description>
+
+               <properties>
+                       <property name="vendor">GoodWe</property>
+               </properties>
+               <representation-property>username</representation-property>
+
+               <config-description>
+                       <parameter name="username" type="text" required="true">
+                               <label>Username</label>
+                               <description>Username (email address) of the account at the SEMS portal</description>
+                       </parameter>
+                       <parameter name="password" type="text" required="true">
+                               <context>password</context>
+                               <label>Password</label>
+                               <description>Password of the SEMS Portal</description>
+                       </parameter>
+                       <parameter name="interval" type="integer" min="1" max="60" unit="min">
+                               <label>Interval</label>
+                               <description>Number of minutes between updates. Minimum is 1 minute, maximum is 60. The default is 5 minutes.</description>
+                               <default>5</default>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="station" listed="false">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="portal"/>
+               </supported-bridge-type-refs>
+
+               <label>Power Station</label>
+               <description>A Power Station is the GoodWe converter that is connected through the internet with the SEMSPortal.</description>
+
+               <channels>
+                       <channel id="lastUpdate" typeId="lastUpdate"/>
+                       <channel id="currentOutput" typeId="currentOutput"/>
+                       <channel id="todayTotal" typeId="todayTotal"/>
+                       <channel id="monthTotal" typeId="monthTotal"/>
+                       <channel id="overallTotal" typeId="overallTotal"/>
+                       <channel id="todayIncome" typeId="todayIncome"/>
+                       <channel id="totalIncome" typeId="totalIncome"/>
+               </channels>
+
+       </thing-type>
+
+
+       <channel-type id="lastUpdate">
+               <item-type>DateTime</item-type>
+               <label>Last Update</label>
+               <description>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.</description>
+       </channel-type>
+       <channel-type id="currentOutput">
+               <item-type>Number:Power</item-type>
+               <label>Current Output</label>
+               <description>Current output in Watts</description>
+       </channel-type>
+       <channel-type id="todayTotal">
+               <item-type>Number:Energy</item-type>
+               <label>Todays Total Output</label>
+               <description>Todays total output in kWh</description>
+       </channel-type>
+       <channel-type id="monthTotal">
+               <item-type>Number:Energy</item-type>
+               <label>Current Month Total Output</label>
+               <description>The total output of this month in kWh</description>
+       </channel-type>
+       <channel-type id="overallTotal">
+               <item-type>Number:Energy</item-type>
+               <label>Overall Total Output</label>
+               <description>The total output from the start of the installation in kWh</description>
+       </channel-type>
+       <channel-type id="todayIncome">
+               <item-type>Number</item-type>
+               <label>Todays Income</label>
+               <description>Todays income. Only reports if you have set the tariffs in the SEMS portal. Unit is the currency that is
+                       set in these tariffs.</description>
+       </channel-type>
+       <channel-type id="totalIncome">
+               <item-type>Number</item-type>
+               <label>Total Income</label>
+               <description>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.</description>
+       </channel-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..b9eaa27
--- /dev/null
@@ -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 (file)
index 0000000..e65e67e
--- /dev/null
@@ -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 (file)
index 0000000..03b7bd5
--- /dev/null
@@ -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 (file)
index 0000000..d9feee9
--- /dev/null
@@ -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 (file)
index 0000000..3c58596
--- /dev/null
@@ -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 (file)
index 0000000..f95b48f
--- /dev/null
@@ -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
index 55732ad1a6f3eb977ea9bd39ec4f3f9fa1148e76..2205dbaa7c12a4c45f18078b0b1263f593952d63 100644 (file)
     <module>org.openhab.binding.sagercaster</module>
     <module>org.openhab.binding.samsungtv</module>
     <module>org.openhab.binding.satel</module>
+    <module>org.openhab.binding.semsportal</module>
     <module>org.openhab.binding.senechome</module>
     <module>org.openhab.binding.seneye</module>
     <module>org.openhab.binding.sensebox</module>