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