]> git.basschouten.com Git - openhab-addons.git/blob
1cdfc31efe0acc83c55bcdda06e670456e81db96
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.netatmo.internal.handler;
14
15 import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
16
17 import java.lang.reflect.InvocationTargetException;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Optional;
25 import java.util.Set;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.stream.Stream;
30
31 import org.apache.oltu.oauth2.client.OAuthClient;
32 import org.apache.oltu.oauth2.client.URLConnectionClient;
33 import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
34 import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse;
35 import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
36 import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
37 import org.apache.oltu.oauth2.common.message.types.GrantType;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.netatmo.internal.config.NetatmoBridgeConfiguration;
41 import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent;
42 import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEventPerson;
43 import org.openhab.binding.netatmo.internal.webhook.WelcomeWebHookServlet;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.Channel;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseBridgeHandler;
51 import org.openhab.core.types.Command;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import io.swagger.client.ApiClient;
56 import io.swagger.client.ApiException;
57 import io.swagger.client.api.HealthyhomecoachApi;
58 import io.swagger.client.api.PartnerApi;
59 import io.swagger.client.api.StationApi;
60 import io.swagger.client.api.ThermostatApi;
61 import io.swagger.client.api.WelcomeApi;
62 import io.swagger.client.auth.Authentication;
63 import io.swagger.client.auth.OAuth;
64 import io.swagger.client.model.NAHealthyHomeCoachDataBody;
65 import io.swagger.client.model.NAMeasureBodyElem;
66 import io.swagger.client.model.NAStationDataBody;
67 import io.swagger.client.model.NAThermostatDataBody;
68 import io.swagger.client.model.NAWelcomeHomeData;
69
70 /**
71  * {@link NetatmoBridgeHandler} is the handler for a Netatmo API and connects it
72  * to the framework. The devices and modules uses the
73  * {@link NetatmoBridgeHandler} to request informations about their status
74  *
75  * @author GaĆ«l L'hopital - Initial contribution OH2 version
76  * @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules
77  *
78  */
79 @NonNullByDefault
80 public class NetatmoBridgeHandler extends BaseBridgeHandler {
81     private final Logger logger = LoggerFactory.getLogger(NetatmoBridgeHandler.class);
82
83     public NetatmoBridgeConfiguration configuration = new NetatmoBridgeConfiguration();
84     private @Nullable ScheduledFuture<?> refreshJob;
85     private @Nullable APICreator apiCreator;
86     private @Nullable WelcomeWebHookServlet webHookServlet;
87     private List<NetatmoDataListener> dataListeners = new CopyOnWriteArrayList<>();
88
89     private static class APICreator {
90
91         private final ApiClient apiClient;
92         private final Map<Class<?>, Object> apiMap;
93
94         private APICreator(ApiClient apiClient) {
95             super();
96             this.apiClient = apiClient;
97             apiMap = new HashMap<>();
98         }
99
100         @SuppressWarnings("unchecked")
101         public <T> T getAPI(Class<T> apiClass) {
102             T api = (T) apiMap.get(apiClass);
103             if (api == null) {
104                 try {
105                     api = apiClass.getDeclaredConstructor(ApiClient.class).newInstance(apiClient);
106                 } catch (InstantiationException | IllegalAccessException | InvocationTargetException
107                         | NoSuchMethodException e) {
108                     throw new RuntimeException("Error on executing API class constructor!", e);
109                 }
110                 apiMap.put(apiClass, api);
111             }
112             return api;
113         }
114     }
115
116     public NetatmoBridgeHandler(Bridge bridge, @Nullable WelcomeWebHookServlet webHookServlet) {
117         super(bridge);
118         this.webHookServlet = webHookServlet;
119     }
120
121     @Override
122     public void initialize() {
123         logger.debug("Initializing Netatmo API bridge handler.");
124
125         configuration = getConfigAs(NetatmoBridgeConfiguration.class);
126         scheduleTokenInitAndRefresh();
127     }
128
129     private void connectionSucceed() {
130         updateStatus(ThingStatus.ONLINE);
131         WelcomeWebHookServlet servlet = webHookServlet;
132         String webHookURI = getWebHookURI();
133         if (servlet != null && webHookURI != null) {
134             getWelcomeApi().ifPresent(api -> {
135                 servlet.activate(this);
136                 logger.debug("Setting up Netatmo Welcome WebHook");
137                 api.addwebhook(webHookURI, WEBHOOK_APP);
138             });
139         }
140     }
141
142     private void scheduleTokenInitAndRefresh() {
143         refreshJob = scheduler.scheduleWithFixedDelay(() -> {
144             logger.debug("Initializing API Connection and scheduling token refresh every {}s",
145                     configuration.reconnectInterval);
146             try {
147                 initializeApiClient();
148                 // I use a connection to Netatmo API using PartnerAPI to ensure that API is reachable
149                 getPartnerApi().partnerdevices();
150                 connectionSucceed();
151             } catch (ApiException e) {
152                 switch (e.getCode()) {
153                     case 404: // If no partner station has been associated - likely to happen - we'll have this
154                               // error
155                               // but it means connection to API is OK
156                         connectionSucceed();
157                         break;
158                     case 403: // Forbidden Access maybe too many requests ? Let's wait next cycle
159                         logger.warn("Error 403 while connecting to Netatmo API, will retry in {} s",
160                                 configuration.reconnectInterval);
161                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162                                 "Netatmo Access Forbidden, will retry in " + configuration.reconnectInterval
163                                         + " seconds.");
164                         break;
165                     default:
166                         if (logger.isDebugEnabled()) {
167                             // we also attach the stack trace
168                             logger.error("Unable to connect Netatmo API : {}", e.getMessage(), e);
169                         } else {
170                             logger.error("Unable to connect Netatmo API : {}", e.getMessage());
171                         }
172                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
173                                 "Unable to connect Netatmo API : " + e.getLocalizedMessage());
174                 }
175             } catch (RuntimeException e) {
176                 logger.warn("Unable to connect Netatmo API : {}", e.getMessage(), e);
177                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
178                         "Netatmo Access Failed, will retry in " + configuration.reconnectInterval + " seconds.");
179             }
180             // We'll do this every x seconds to guaranty token refresh
181         }, 2, configuration.reconnectInterval, TimeUnit.SECONDS);
182     }
183
184     private void initializeApiClient() {
185         try {
186             ApiClient apiClient = new ApiClient();
187
188             OAuthClientRequest oAuthRequest = OAuthClientRequest.tokenLocation("https://api.netatmo.net/oauth2/token")
189                     .setClientId(configuration.clientId).setClientSecret(configuration.clientSecret)
190                     .setUsername(configuration.username).setPassword(configuration.password).setScope(getApiScope())
191                     .setGrantType(GrantType.PASSWORD).buildBodyMessage();
192
193             OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
194
195             OAuthJSONAccessTokenResponse accessTokenResponse = oAuthClient.accessToken(oAuthRequest,
196                     OAuthJSONAccessTokenResponse.class);
197             String accessToken = accessTokenResponse.getAccessToken();
198
199             for (Authentication authentication : apiClient.getAuthentications().values()) {
200                 if (authentication instanceof OAuth) {
201                     ((OAuth) authentication).setAccessToken(accessToken);
202                 }
203             }
204
205             apiCreator = new APICreator(apiClient);
206         } catch (OAuthSystemException | OAuthProblemException e) {
207             throw new RuntimeException("Error on trying to get an access token!", e);
208         }
209     }
210
211     private String getApiScope() {
212         List<String> scopes = new ArrayList<>();
213
214         if (configuration.readStation) {
215             scopes.add("read_station");
216         }
217
218         if (configuration.readThermostat) {
219             scopes.add("read_thermostat");
220             scopes.add("write_thermostat");
221         }
222
223         if (configuration.readHealthyHomeCoach) {
224             scopes.add("read_homecoach");
225         }
226
227         if (configuration.readWelcome) {
228             scopes.add("read_camera");
229             scopes.add("access_camera");
230             scopes.add("write_camera");
231         }
232
233         if (configuration.readPresence) {
234             scopes.add("read_presence");
235             scopes.add("access_presence");
236         }
237
238         return String.join(" ", scopes);
239     }
240
241     @Override
242     public void handleCommand(ChannelUID channelUID, Command command) {
243         logger.debug("Netatmo Bridge is read-only and does not handle commands");
244     }
245
246     public @Nullable PartnerApi getPartnerApi() {
247         return apiCreator != null ? apiCreator.getAPI(PartnerApi.class) : null;
248     }
249
250     public Optional<StationApi> getStationApi() {
251         return apiCreator != null ? Optional.of(apiCreator.getAPI(StationApi.class)) : Optional.empty();
252     }
253
254     public Optional<HealthyhomecoachApi> getHomeCoachApi() {
255         return apiCreator != null ? Optional.of(apiCreator.getAPI(HealthyhomecoachApi.class)) : Optional.empty();
256     }
257
258     public Optional<ThermostatApi> getThermostatApi() {
259         return apiCreator != null ? Optional.of(apiCreator.getAPI(ThermostatApi.class)) : Optional.empty();
260     }
261
262     public Optional<WelcomeApi> getWelcomeApi() {
263         return apiCreator != null ? Optional.of(apiCreator.getAPI(WelcomeApi.class)) : Optional.empty();
264     }
265
266     @Override
267     public void dispose() {
268         logger.debug("Running dispose()");
269
270         WelcomeWebHookServlet servlet = webHookServlet;
271         if (servlet != null && getWebHookURI() != null) {
272             getWelcomeApi().ifPresent(api -> {
273                 logger.debug("Releasing Netatmo Welcome WebHook");
274                 servlet.deactivate();
275                 api.dropwebhook(WEBHOOK_APP);
276             });
277         }
278
279         ScheduledFuture<?> job = refreshJob;
280         if (job != null) {
281             job.cancel(true);
282             refreshJob = null;
283         }
284     }
285
286     public Optional<NAStationDataBody> getStationsDataBody(@Nullable String equipmentId) {
287         Optional<NAStationDataBody> data = getStationApi().map(api -> api.getstationsdata(equipmentId, true).getBody());
288         updateStatus(ThingStatus.ONLINE);
289         return data;
290     }
291
292     public List<Float> getStationMeasureResponses(String equipmentId, @Nullable String moduleId, String scale,
293             List<String> types) {
294         List<NAMeasureBodyElem> data = getStationApi()
295                 .map(api -> api.getmeasure(equipmentId, scale, types, moduleId, null, "last", 1, true, false).getBody())
296                 .orElse(null);
297         updateStatus(ThingStatus.ONLINE);
298         NAMeasureBodyElem element = (data != null && data.size() > 0) ? data.get(0) : null;
299         return element != null ? element.getValue().get(0) : Collections.emptyList();
300     }
301
302     public Optional<NAHealthyHomeCoachDataBody> getHomecoachDataBody(@Nullable String equipmentId) {
303         Optional<NAHealthyHomeCoachDataBody> data = getHomeCoachApi()
304                 .map(api -> api.gethomecoachsdata(equipmentId).getBody());
305         updateStatus(ThingStatus.ONLINE);
306         return data;
307     }
308
309     public Optional<NAThermostatDataBody> getThermostatsDataBody(@Nullable String equipmentId) {
310         Optional<NAThermostatDataBody> data = getThermostatApi()
311                 .map(api -> api.getthermostatsdata(equipmentId).getBody());
312         updateStatus(ThingStatus.ONLINE);
313         return data;
314     }
315
316     public Optional<NAWelcomeHomeData> getWelcomeDataBody(@Nullable String homeId) {
317         Optional<NAWelcomeHomeData> data = getWelcomeApi().map(api -> api.gethomedata(homeId, null).getBody());
318         updateStatus(ThingStatus.ONLINE);
319         return data;
320     }
321
322     /**
323      * Returns the Url of the picture
324      *
325      * @return Url of the picture or UnDefType.UNDEF
326      */
327     public String getPictureUrl(@Nullable String id, @Nullable String key) {
328         StringBuilder ret = new StringBuilder();
329         if (id != null && key != null) {
330             ret.append(WELCOME_PICTURE_URL).append("?").append(WELCOME_PICTURE_IMAGEID).append("=").append(id)
331                     .append("&").append(WELCOME_PICTURE_KEY).append("=").append(key);
332         }
333         return ret.toString();
334     }
335
336     public Optional<AbstractNetatmoThingHandler> findNAThing(@Nullable String searchedId) {
337         List<Thing> things = getThing().getThings();
338         Stream<AbstractNetatmoThingHandler> naHandlers = things.stream().map(Thing::getHandler)
339                 .filter(AbstractNetatmoThingHandler.class::isInstance).map(AbstractNetatmoThingHandler.class::cast)
340                 .filter(handler -> handler.matchesId(searchedId));
341         return naHandlers.findAny();
342     }
343
344     public void webHookEvent(NAWebhookCameraEvent event) {
345         // This currently the only known event type but I suspect usage can grow in the future...
346         if (event.getAppType() == NAWebhookCameraEvent.AppTypeEnum.CAMERA) {
347             Set<AbstractNetatmoThingHandler> modules = new HashSet<>();
348             if (WELCOME_EVENTS.contains(event.getEventType()) || PRESENCE_EVENTS.contains(event.getEventType())) {
349                 String cameraId = event.getCameraId();
350                 if (cameraId != null) {
351                     Optional<AbstractNetatmoThingHandler> camera = findNAThing(cameraId);
352                     camera.ifPresent(modules::add);
353                 }
354             }
355             if (HOME_EVENTS.contains(event.getEventType())) {
356                 String homeId = event.getHomeId();
357                 if (homeId != null) {
358                     Optional<AbstractNetatmoThingHandler> home = findNAThing(homeId);
359                     home.ifPresent(modules::add);
360                 }
361             }
362             if (PERSON_EVENTS.contains(event.getEventType())) {
363                 List<NAWebhookCameraEventPerson> persons = event.getPersons();
364                 persons.forEach(person -> {
365                     String personId = person.getId();
366                     if (personId != null) {
367                         Optional<AbstractNetatmoThingHandler> personHandler = findNAThing(personId);
368                         personHandler.ifPresent(modules::add);
369                     }
370                 });
371             }
372             modules.forEach(module -> {
373                 Channel channel = module.getThing().getChannel(CHANNEL_WELCOME_HOME_EVENT);
374                 if (channel != null) {
375                     triggerChannel(channel.getUID(), event.getEventType().toString());
376                 }
377             });
378         }
379     }
380
381     private @Nullable String getWebHookURI() {
382         String webHookURI = null;
383         WelcomeWebHookServlet webHookServlet = this.webHookServlet;
384         if (configuration.webHookUrl != null && (configuration.readWelcome || configuration.readPresence)
385                 && webHookServlet != null) {
386             webHookURI = configuration.webHookUrl + webHookServlet.getPath();
387         }
388         return webHookURI;
389     }
390
391     public boolean registerDataListener(NetatmoDataListener dataListener) {
392         return dataListeners.add(dataListener);
393     }
394
395     public boolean unregisterDataListener(NetatmoDataListener dataListener) {
396         return dataListeners.remove(dataListener);
397     }
398
399     public void checkForNewThings(Object data) {
400         for (NetatmoDataListener dataListener : dataListeners) {
401             dataListener.onDataRefreshed(data);
402         }
403     }
404 }