]> git.basschouten.com Git - openhab-addons.git/blob
de6d8febfceb9f5ffc64299f54eda427fb001e69
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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                 if (logger.isDebugEnabled()) {
177                     logger.warn("Unable to connect Netatmo API : {}", e.getMessage(), e);
178                 } else {
179                     logger.warn("Unable to connect Netatmo API : {}", e.getMessage());
180                 }
181                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
182                         "Netatmo Access Failed, will retry in " + configuration.reconnectInterval + " seconds.");
183             }
184             // We'll do this every x seconds to guaranty token refresh
185         }, 2, configuration.reconnectInterval, TimeUnit.SECONDS);
186     }
187
188     private void initializeApiClient() {
189         try {
190             ApiClient apiClient = new ApiClient();
191
192             OAuthClientRequest oAuthRequest = OAuthClientRequest.tokenLocation("https://api.netatmo.net/oauth2/token")
193                     .setClientId(configuration.clientId).setClientSecret(configuration.clientSecret)
194                     .setUsername(configuration.username).setPassword(configuration.password).setScope(getApiScope())
195                     .setGrantType(GrantType.PASSWORD).buildBodyMessage();
196
197             OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
198
199             OAuthJSONAccessTokenResponse accessTokenResponse = oAuthClient.accessToken(oAuthRequest,
200                     OAuthJSONAccessTokenResponse.class);
201             String accessToken = accessTokenResponse.getAccessToken();
202
203             for (Authentication authentication : apiClient.getAuthentications().values()) {
204                 if (authentication instanceof OAuth) {
205                     ((OAuth) authentication).setAccessToken(accessToken);
206                 }
207             }
208
209             apiCreator = new APICreator(apiClient);
210         } catch (OAuthSystemException | OAuthProblemException e) {
211             throw new RuntimeException("Error on trying to get an access token!", e);
212         }
213     }
214
215     private String getApiScope() {
216         List<String> scopes = new ArrayList<>();
217
218         if (configuration.readStation) {
219             scopes.add("read_station");
220         }
221
222         if (configuration.readThermostat) {
223             scopes.add("read_thermostat");
224             scopes.add("write_thermostat");
225         }
226
227         if (configuration.readHealthyHomeCoach) {
228             scopes.add("read_homecoach");
229         }
230
231         if (configuration.readWelcome) {
232             scopes.add("read_camera");
233             scopes.add("access_camera");
234             scopes.add("write_camera");
235         }
236
237         if (configuration.readPresence) {
238             scopes.add("read_presence");
239             scopes.add("access_presence");
240         }
241
242         return String.join(" ", scopes);
243     }
244
245     @Override
246     public void handleCommand(ChannelUID channelUID, Command command) {
247         logger.debug("Netatmo Bridge is read-only and does not handle commands");
248     }
249
250     public @Nullable PartnerApi getPartnerApi() {
251         return apiCreator != null ? apiCreator.getAPI(PartnerApi.class) : null;
252     }
253
254     public Optional<StationApi> getStationApi() {
255         return apiCreator != null ? Optional.of(apiCreator.getAPI(StationApi.class)) : Optional.empty();
256     }
257
258     public Optional<HealthyhomecoachApi> getHomeCoachApi() {
259         return apiCreator != null ? Optional.of(apiCreator.getAPI(HealthyhomecoachApi.class)) : Optional.empty();
260     }
261
262     public Optional<ThermostatApi> getThermostatApi() {
263         return apiCreator != null ? Optional.of(apiCreator.getAPI(ThermostatApi.class)) : Optional.empty();
264     }
265
266     public Optional<WelcomeApi> getWelcomeApi() {
267         return apiCreator != null ? Optional.of(apiCreator.getAPI(WelcomeApi.class)) : Optional.empty();
268     }
269
270     @Override
271     public void dispose() {
272         logger.debug("Running dispose()");
273
274         WelcomeWebHookServlet servlet = webHookServlet;
275         if (servlet != null && getWebHookURI() != null) {
276             getWelcomeApi().ifPresent(api -> {
277                 logger.debug("Releasing Netatmo Welcome WebHook");
278                 servlet.deactivate();
279                 api.dropwebhook(WEBHOOK_APP);
280             });
281         }
282
283         ScheduledFuture<?> job = refreshJob;
284         if (job != null) {
285             job.cancel(true);
286             refreshJob = null;
287         }
288     }
289
290     public Optional<NAStationDataBody> getStationsDataBody(@Nullable String equipmentId) {
291         Optional<NAStationDataBody> data = getStationApi().map(api -> api.getstationsdata(equipmentId, true).getBody());
292         updateStatus(ThingStatus.ONLINE);
293         return data;
294     }
295
296     public List<Float> getStationMeasureResponses(String equipmentId, @Nullable String moduleId, String scale,
297             List<String> types) {
298         List<NAMeasureBodyElem> data = getStationApi()
299                 .map(api -> api.getmeasure(equipmentId, scale, types, moduleId, null, "last", 1, true, false).getBody())
300                 .orElse(null);
301         updateStatus(ThingStatus.ONLINE);
302         NAMeasureBodyElem element = data != null && !data.isEmpty() ? data.get(0) : null;
303         return element != null ? element.getValue().get(0) : Collections.emptyList();
304     }
305
306     public Optional<NAHealthyHomeCoachDataBody> getHomecoachDataBody(@Nullable String equipmentId) {
307         Optional<NAHealthyHomeCoachDataBody> data = getHomeCoachApi()
308                 .map(api -> api.gethomecoachsdata(equipmentId).getBody());
309         updateStatus(ThingStatus.ONLINE);
310         return data;
311     }
312
313     public Optional<NAThermostatDataBody> getThermostatsDataBody(@Nullable String equipmentId) {
314         Optional<NAThermostatDataBody> data = getThermostatApi()
315                 .map(api -> api.getthermostatsdata(equipmentId).getBody());
316         updateStatus(ThingStatus.ONLINE);
317         return data;
318     }
319
320     public Optional<NAWelcomeHomeData> getWelcomeDataBody(@Nullable String homeId) {
321         Optional<NAWelcomeHomeData> data = getWelcomeApi().map(api -> api.gethomedata(homeId, null).getBody());
322         updateStatus(ThingStatus.ONLINE);
323         return data;
324     }
325
326     /**
327      * Returns the Url of the picture
328      *
329      * @return Url of the picture or UnDefType.UNDEF
330      */
331     public String getPictureUrl(@Nullable String id, @Nullable String key) {
332         StringBuilder ret = new StringBuilder();
333         if (id != null && key != null) {
334             ret.append(WELCOME_PICTURE_URL).append("?").append(WELCOME_PICTURE_IMAGEID).append("=").append(id)
335                     .append("&").append(WELCOME_PICTURE_KEY).append("=").append(key);
336         }
337         return ret.toString();
338     }
339
340     public Optional<AbstractNetatmoThingHandler> findNAThing(@Nullable String searchedId) {
341         List<Thing> things = getThing().getThings();
342         Stream<AbstractNetatmoThingHandler> naHandlers = things.stream().map(Thing::getHandler)
343                 .filter(AbstractNetatmoThingHandler.class::isInstance).map(AbstractNetatmoThingHandler.class::cast)
344                 .filter(handler -> handler.matchesId(searchedId));
345         return naHandlers.findAny();
346     }
347
348     public void webHookEvent(NAWebhookCameraEvent event) {
349         // This currently the only known event type but I suspect usage can grow in the future...
350         if (event.getAppType() == NAWebhookCameraEvent.AppTypeEnum.CAMERA) {
351             Set<AbstractNetatmoThingHandler> modules = new HashSet<>();
352             if (WELCOME_EVENTS.contains(event.getEventType()) || PRESENCE_EVENTS.contains(event.getEventType())) {
353                 String cameraId = event.getCameraId();
354                 if (cameraId != null) {
355                     Optional<AbstractNetatmoThingHandler> camera = findNAThing(cameraId);
356                     camera.ifPresent(modules::add);
357                 }
358             }
359             if (HOME_EVENTS.contains(event.getEventType())) {
360                 String homeId = event.getHomeId();
361                 if (homeId != null) {
362                     Optional<AbstractNetatmoThingHandler> home = findNAThing(homeId);
363                     home.ifPresent(modules::add);
364                 }
365             }
366             if (PERSON_EVENTS.contains(event.getEventType())) {
367                 List<NAWebhookCameraEventPerson> persons = event.getPersons();
368                 persons.forEach(person -> {
369                     String personId = person.getId();
370                     if (personId != null) {
371                         Optional<AbstractNetatmoThingHandler> personHandler = findNAThing(personId);
372                         personHandler.ifPresent(modules::add);
373                     }
374                 });
375             }
376             modules.forEach(module -> {
377                 Channel channel = module.getThing().getChannel(CHANNEL_WELCOME_HOME_EVENT);
378                 if (channel != null) {
379                     triggerChannel(channel.getUID(), event.getEventType().toString());
380                 }
381             });
382         }
383     }
384
385     private @Nullable String getWebHookURI() {
386         String webHookURI = null;
387         WelcomeWebHookServlet webHookServlet = this.webHookServlet;
388         if (configuration.webHookUrl != null && (configuration.readWelcome || configuration.readPresence)
389                 && webHookServlet != null) {
390             webHookURI = configuration.webHookUrl + webHookServlet.getPath();
391         }
392         return webHookURI;
393     }
394
395     public boolean registerDataListener(NetatmoDataListener dataListener) {
396         return dataListeners.add(dataListener);
397     }
398
399     public boolean unregisterDataListener(NetatmoDataListener dataListener) {
400         return dataListeners.remove(dataListener);
401     }
402
403     public void checkForNewThings(Object data) {
404         for (NetatmoDataListener dataListener : dataListeners) {
405             dataListener.onDataRefreshed(data);
406         }
407     }
408 }