2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.semsportal.internal;
15 import java.nio.charset.StandardCharsets;
16 import java.util.Collections;
17 import java.util.List;
18 import java.util.concurrent.ScheduledFuture;
20 import javax.ws.rs.core.MediaType;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.eclipse.jetty.client.api.ContentResponse;
26 import org.eclipse.jetty.client.api.Request;
27 import org.eclipse.jetty.client.util.StringContentProvider;
28 import org.eclipse.jetty.http.HttpHeader;
29 import org.openhab.binding.semsportal.internal.dto.BaseResponse;
30 import org.openhab.binding.semsportal.internal.dto.LoginRequest;
31 import org.openhab.binding.semsportal.internal.dto.LoginResponse;
32 import org.openhab.binding.semsportal.internal.dto.SEMSToken;
33 import org.openhab.binding.semsportal.internal.dto.Station;
34 import org.openhab.binding.semsportal.internal.dto.StationListRequest;
35 import org.openhab.binding.semsportal.internal.dto.StationListResponse;
36 import org.openhab.binding.semsportal.internal.dto.StationStatus;
37 import org.openhab.binding.semsportal.internal.dto.StatusRequest;
38 import org.openhab.binding.semsportal.internal.dto.StatusResponse;
39 import org.openhab.core.io.net.http.HttpClientFactory;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseBridgeHandler;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import com.google.gson.Gson;
50 import com.google.gson.GsonBuilder;
53 * The {@link PortalHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Iwan Bron - Initial contribution
59 public class PortalHandler extends BaseBridgeHandler {
60 private Logger logger = LoggerFactory.getLogger(PortalHandler.class);
61 // the settings that are needed when you do not have avalid session token yet
62 private static final SEMSToken SESSIONLESS_TOKEN = new SEMSToken("v2.1.0", "ios", "en");
63 // the url of the SEMS portal API
64 private static final String BASE_URL = "https://www.semsportal.com/";
65 // url for the login request, to get a valid session token
66 private static final String LOGIN_URL = BASE_URL + "api/v2/Common/CrossLogin";
67 // url to get the status of a specific power station
68 private static final String STATUS_URL = BASE_URL + "api/v2/PowerStation/GetMonitorDetailByPowerstationId";
69 private static final String LIST_URL = BASE_URL + "api/PowerStationMonitor/QueryPowerStationMonitorForApp";
70 // the token holds the credential information for the portal
71 private static final String HTTP_HEADER_TOKEN = "Token";
73 // used to parse json from / to the SEMS portal API
74 private final Gson gson;
75 private final HttpClient httpClient;
77 // configuration as provided by the openhab framework: initialize with defaults to prevent compiler check errors
78 private SEMSPortalConfiguration config = new SEMSPortalConfiguration();
79 private boolean loggedIn;
80 private SEMSToken sessionToken = SESSIONLESS_TOKEN;// gets the default, it is needed for the login
81 private @Nullable StationStatus currentStatus;
83 private @Nullable ScheduledFuture<?> pollingJob;
85 public PortalHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
87 httpClient = httpClientFactory.getCommonHttpClient();
88 gson = new GsonBuilder().create();
92 public void handleCommand(ChannelUID channelUID, Command command) {
93 logger.debug("No supported commands. Ignoring command {} for channel {}", command, channelUID);
98 public void initialize() {
99 config = getConfigAs(SEMSPortalConfiguration.class);
100 updateStatus(ThingStatus.UNKNOWN);
102 scheduler.execute(() -> {
105 } catch (Exception e) {
106 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
107 "Error when loggin in. Check your username and password");
113 public void dispose() {
114 ScheduledFuture<?> localPollingJob = pollingJob;
115 if (localPollingJob != null) {
116 localPollingJob.cancel(true);
121 private void login() {
123 String payload = gson.toJson(new LoginRequest(config.username, config.password));
124 String response = sendPost(LOGIN_URL, payload);
125 if (response == null) {
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
127 "Invalid response from SEMS portal");
130 LoginResponse loginResponse = gson.fromJson(response, LoginResponse.class);
131 if (loginResponse == null) {
132 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Check username / password");
135 if (loginResponse.isOk()) {
136 logger.debug("Successfuly logged in to SEMS portal");
137 if (loginResponse.getToken() != null) {
138 sessionToken = loginResponse.getToken();
141 updateStatus(ThingStatus.ONLINE);
143 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
144 "Check username / password");
148 private @Nullable String sendPost(String url, String payload) {
150 Request request = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON)
151 .header(HTTP_HEADER_TOKEN, gson.toJson(sessionToken))
152 .content(new StringContentProvider(payload, StandardCharsets.UTF_8.name()),
153 MediaType.APPLICATION_JSON);
154 request.getHeaders().remove(HttpHeader.ACCEPT_ENCODING);
155 ContentResponse response = request.send();
156 logger.trace("received response: {}", response.getContentAsString());
157 return response.getContentAsString();
158 } catch (Exception e) {
159 logger.debug("{} when posting to url {}", e.getClass().getSimpleName(), url, e);
164 public boolean isLoggedIn() {
168 public @Nullable StationStatus getStationStatus(String stationUUID)
169 throws CommunicationException, ConfigurationException {
171 logger.debug("Not logged in. Not updating.");
174 String response = sendPost(STATUS_URL, gson.toJson(new StatusRequest(stationUUID)));
175 if (response == null) {
176 throw new CommunicationException("No response received from portal");
178 BaseResponse semsResponse = gson.fromJson(response, BaseResponse.class);
179 if (semsResponse == null) {
180 throw new CommunicationException("Portal reponse not understood");
182 if (semsResponse.isOk()) {
183 StatusResponse statusResponse = gson.fromJson(response, StatusResponse.class);
184 if (statusResponse == null) {
185 throw new CommunicationException("Portal reponse not understood");
187 currentStatus = statusResponse.getStatus();
188 updateStatus(ThingStatus.ONLINE); // we got a valid response, register as online
189 return currentStatus;
190 } else if (semsResponse.isSessionInvalid()) {
191 logger.debug("Session is invalidated. Attempting new login.");
193 return getStationStatus(stationUUID);
194 } else if (semsResponse.isError()) {
195 throw new ConfigurationException(
196 "ERROR status code received from SEMS portal. Please check your station ID");
198 throw new CommunicationException(String.format("Unknown status code received from SEMS portal: %s - %s",
199 semsResponse.getCode(), semsResponse.getMsg()));
203 public long getUpdateInterval() {
204 return config.interval;
207 public List<Station> getAllStations() {
208 String payload = gson.toJson(new StationListRequest());
209 String response = sendPost(LIST_URL, payload);
210 if (response == null) {
211 logger.debug("Received empty list stations response from SEMS portal");
212 return Collections.emptyList();
214 StationListResponse listResponse = gson.fromJson(response, StationListResponse.class);
215 if (listResponse == null) {
216 logger.debug("Unable to read list stations response from SEMS portal");
217 return Collections.emptyList();
219 if (listResponse.isOk()) {
220 logger.debug("Received list of {} stations from SEMS portal", listResponse.getStations().size());
222 updateStatus(ThingStatus.ONLINE);
223 return listResponse.getStations();
225 logger.debug("Received error response with code {} and message {} from SEMS portal", listResponse.getCode(),
226 listResponse.getMsg());
227 return Collections.emptyList();