2 * Copyright (c) 2010-2020 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.netatmo.internal.handler;
15 import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
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;
24 import java.util.Optional;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.stream.Stream;
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;
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;
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
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
80 public class NetatmoBridgeHandler extends BaseBridgeHandler {
81 private final Logger logger = LoggerFactory.getLogger(NetatmoBridgeHandler.class);
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<>();
89 private static class APICreator {
91 private final ApiClient apiClient;
92 private final Map<Class<?>, Object> apiMap;
94 private APICreator(ApiClient apiClient) {
96 this.apiClient = apiClient;
97 apiMap = new HashMap<>();
100 @SuppressWarnings("unchecked")
101 public <T> T getAPI(Class<T> apiClass) {
102 T api = (T) apiMap.get(apiClass);
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);
110 apiMap.put(apiClass, api);
116 public NetatmoBridgeHandler(Bridge bridge, @Nullable WelcomeWebHookServlet webHookServlet) {
118 this.webHookServlet = webHookServlet;
122 public void initialize() {
123 logger.debug("Initializing Netatmo API bridge handler.");
125 configuration = getConfigAs(NetatmoBridgeConfiguration.class);
126 scheduleTokenInitAndRefresh();
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);
142 private void scheduleTokenInitAndRefresh() {
143 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
144 logger.debug("Initializing API Connection and scheduling token refresh every {}s",
145 configuration.reconnectInterval);
147 initializeApiClient();
148 // I use a connection to Netatmo API using PartnerAPI to ensure that API is reachable
149 getPartnerApi().partnerdevices();
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
155 // but it means connection to API is OK
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
166 if (logger.isDebugEnabled()) {
167 // we also attach the stack trace
168 logger.error("Unable to connect Netatmo API : {}", e.getMessage(), e);
170 logger.error("Unable to connect Netatmo API : {}", e.getMessage());
172 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
173 "Unable to connect Netatmo API : " + e.getLocalizedMessage());
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.");
180 // We'll do this every x seconds to guaranty token refresh
181 }, 2, configuration.reconnectInterval, TimeUnit.SECONDS);
184 private void initializeApiClient() {
186 ApiClient apiClient = new ApiClient();
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();
193 OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
195 OAuthJSONAccessTokenResponse accessTokenResponse = oAuthClient.accessToken(oAuthRequest,
196 OAuthJSONAccessTokenResponse.class);
197 String accessToken = accessTokenResponse.getAccessToken();
199 for (Authentication authentication : apiClient.getAuthentications().values()) {
200 if (authentication instanceof OAuth) {
201 ((OAuth) authentication).setAccessToken(accessToken);
205 apiCreator = new APICreator(apiClient);
206 } catch (OAuthSystemException | OAuthProblemException e) {
207 throw new RuntimeException("Error on trying to get an access token!", e);
211 private String getApiScope() {
212 List<String> scopes = new ArrayList<>();
214 if (configuration.readStation) {
215 scopes.add("read_station");
218 if (configuration.readThermostat) {
219 scopes.add("read_thermostat");
220 scopes.add("write_thermostat");
223 if (configuration.readHealthyHomeCoach) {
224 scopes.add("read_homecoach");
227 if (configuration.readWelcome) {
228 scopes.add("read_camera");
229 scopes.add("access_camera");
230 scopes.add("write_camera");
233 if (configuration.readPresence) {
234 scopes.add("read_presence");
235 scopes.add("access_presence");
238 return String.join(" ", scopes);
242 public void handleCommand(ChannelUID channelUID, Command command) {
243 logger.debug("Netatmo Bridge is read-only and does not handle commands");
246 public @Nullable PartnerApi getPartnerApi() {
247 return apiCreator != null ? apiCreator.getAPI(PartnerApi.class) : null;
250 public Optional<StationApi> getStationApi() {
251 return apiCreator != null ? Optional.of(apiCreator.getAPI(StationApi.class)) : Optional.empty();
254 public Optional<HealthyhomecoachApi> getHomeCoachApi() {
255 return apiCreator != null ? Optional.of(apiCreator.getAPI(HealthyhomecoachApi.class)) : Optional.empty();
258 public Optional<ThermostatApi> getThermostatApi() {
259 return apiCreator != null ? Optional.of(apiCreator.getAPI(ThermostatApi.class)) : Optional.empty();
262 public Optional<WelcomeApi> getWelcomeApi() {
263 return apiCreator != null ? Optional.of(apiCreator.getAPI(WelcomeApi.class)) : Optional.empty();
267 public void dispose() {
268 logger.debug("Running dispose()");
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);
279 ScheduledFuture<?> job = refreshJob;
286 public Optional<NAStationDataBody> getStationsDataBody(@Nullable String equipmentId) {
287 Optional<NAStationDataBody> data = getStationApi().map(api -> api.getstationsdata(equipmentId, true).getBody());
288 updateStatus(ThingStatus.ONLINE);
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())
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();
302 public Optional<NAHealthyHomeCoachDataBody> getHomecoachDataBody(@Nullable String equipmentId) {
303 Optional<NAHealthyHomeCoachDataBody> data = getHomeCoachApi()
304 .map(api -> api.gethomecoachsdata(equipmentId).getBody());
305 updateStatus(ThingStatus.ONLINE);
309 public Optional<NAThermostatDataBody> getThermostatsDataBody(@Nullable String equipmentId) {
310 Optional<NAThermostatDataBody> data = getThermostatApi()
311 .map(api -> api.getthermostatsdata(equipmentId).getBody());
312 updateStatus(ThingStatus.ONLINE);
316 public Optional<NAWelcomeHomeData> getWelcomeDataBody(@Nullable String homeId) {
317 Optional<NAWelcomeHomeData> data = getWelcomeApi().map(api -> api.gethomedata(homeId, null).getBody());
318 updateStatus(ThingStatus.ONLINE);
323 * Returns the Url of the picture
325 * @return Url of the picture or UnDefType.UNDEF
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);
333 return ret.toString();
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();
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);
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);
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);
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());
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();
391 public boolean registerDataListener(NetatmoDataListener dataListener) {
392 return dataListeners.add(dataListener);
395 public boolean unregisterDataListener(NetatmoDataListener dataListener) {
396 return dataListeners.remove(dataListener);
399 public void checkForNewThings(Object data) {
400 for (NetatmoDataListener dataListener : dataListeners) {
401 dataListener.onDataRefreshed(data);