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