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.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;
24 import java.util.concurrent.CopyOnWriteArrayList;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.stream.Stream;
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;
47 import com.squareup.okhttp.OkHttpClient;
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;
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
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
77 public class NetatmoBridgeHandler extends BaseBridgeHandler {
78 private final Logger logger = LoggerFactory.getLogger(NetatmoBridgeHandler.class);
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<>();
86 private class APIMap extends HashMap<Class<?>, Object> {
87 private static final long serialVersionUID = -2024031764691952343L;
88 private ApiClient apiClient;
90 public APIMap(ApiClient apiClient) {
92 this.apiClient = apiClient;
95 public Object get(Class<?> apiClass) {
96 if (!super.containsKey(apiClass)) {
97 Object api = apiClient.createService(apiClass);
98 super.put(apiClass, api);
100 return super.get(apiClass);
104 public NetatmoBridgeHandler(Bridge bridge, @Nullable WelcomeWebHookServlet webHookServlet) {
106 this.webHookServlet = webHookServlet;
110 public void initialize() {
111 logger.debug("Initializing Netatmo API bridge handler.");
113 configuration = getConfigAs(NetatmoBridgeConfiguration.class);
114 scheduleTokenInitAndRefresh();
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);
130 private void scheduleTokenInitAndRefresh() {
131 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
132 logger.debug("Initializing API Connection and scheduling token refresh every {}s",
133 configuration.reconnectInterval);
135 initializeApiClient();
136 // I use a connection to Netatmo API using PartnerAPI to ensure that API is reachable
137 getPartnerApi().partnerdevices();
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.");
146 switch (e.getResponse().getStatus()) {
147 case 404: // If no partner station has been associated - likely to happen - we'll have this
149 // but it means connection to API is OK
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
160 if (logger.isDebugEnabled()) {
161 // we also attach the stack trace
162 logger.error("Unable to connect Netatmo API : {}", e.getMessage(), e);
164 logger.error("Unable to connect Netatmo API : {}", e.getMessage());
166 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
167 "Unable to connect Netatmo API : " + e.getLocalizedMessage());
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.");
176 // We'll do this every x seconds to guaranty token refresh
177 }, 2, configuration.reconnectInterval, TimeUnit.SECONDS);
180 private void initializeApiClient() throws RetrofitError {
181 ApiClient apiClient = new ApiClient();
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(""));
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());
192 apiClient.configureFromOkclient(new OkHttpClient());
193 apiClient.getAdapterBuilder().setLogLevel(logger.isDebugEnabled() ? LogLevel.FULL : LogLevel.NONE);
195 apiMap = new APIMap(apiClient);
198 private String getApiScope() {
199 List<String> scopes = new ArrayList<>();
201 if (configuration.readStation) {
202 scopes.add("read_station");
205 if (configuration.readThermostat) {
206 scopes.add("read_thermostat");
207 scopes.add("write_thermostat");
210 if (configuration.readHealthyHomeCoach) {
211 scopes.add("read_homecoach");
214 if (configuration.readWelcome) {
215 scopes.add("read_camera");
216 scopes.add("access_camera");
217 scopes.add("write_camera");
220 if (configuration.readPresence) {
221 scopes.add("read_presence");
222 scopes.add("access_presence");
225 return String.join(" ", scopes);
229 public void handleCommand(ChannelUID channelUID, Command command) {
230 logger.debug("Netatmo Bridge is read-only and does not handle commands");
233 public @Nullable PartnerApi getPartnerApi() {
235 return map != null ? (PartnerApi) map.get(PartnerApi.class) : null;
238 public Optional<StationApi> getStationApi() {
240 return map != null ? Optional.of((StationApi) map.get(StationApi.class)) : Optional.empty();
243 public Optional<HealthyhomecoachApi> getHomeCoachApi() {
245 return map != null ? Optional.of((HealthyhomecoachApi) map.get(HealthyhomecoachApi.class)) : Optional.empty();
248 public Optional<ThermostatApi> getThermostatApi() {
250 return map != null ? Optional.of((ThermostatApi) map.get(ThermostatApi.class)) : Optional.empty();
253 public Optional<WelcomeApi> getWelcomeApi() {
255 return map != null ? Optional.of((WelcomeApi) map.get(WelcomeApi.class)) : Optional.empty();
259 public void dispose() {
260 logger.debug("Running dispose()");
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);
271 ScheduledFuture<?> job = refreshJob;
278 public Optional<NAStationDataBody> getStationsDataBody(@Nullable String equipmentId) {
279 Optional<NAStationDataBody> data = getStationApi().map(api -> api.getstationsdata(equipmentId, true).getBody());
280 updateStatus(ThingStatus.ONLINE);
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())
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();
294 public Optional<NAHealthyHomeCoachDataBody> getHomecoachDataBody(@Nullable String equipmentId) {
295 Optional<NAHealthyHomeCoachDataBody> data = getHomeCoachApi()
296 .map(api -> api.gethomecoachsdata(equipmentId).getBody());
297 updateStatus(ThingStatus.ONLINE);
301 public Optional<NAThermostatDataBody> getThermostatsDataBody(@Nullable String equipmentId) {
302 Optional<NAThermostatDataBody> data = getThermostatApi()
303 .map(api -> api.getthermostatsdata(equipmentId).getBody());
304 updateStatus(ThingStatus.ONLINE);
308 public Optional<NAWelcomeHomeData> getWelcomeDataBody(@Nullable String homeId) {
309 Optional<NAWelcomeHomeData> data = getWelcomeApi().map(api -> api.gethomedata(homeId, null).getBody());
310 updateStatus(ThingStatus.ONLINE);
315 * Returns the Url of the picture
317 * @return Url of the picture or UnDefType.UNDEF
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);
325 return ret.toString();
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();
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);
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);
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);
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());
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();
383 public boolean registerDataListener(NetatmoDataListener dataListener) {
384 return dataListeners.add(dataListener);
387 public boolean unregisterDataListener(NetatmoDataListener dataListener) {
388 return dataListeners.remove(dataListener);
391 public void checkForNewThings(Object data) {
392 for (NetatmoDataListener dataListener : dataListeners) {
393 dataListener.onDataRefreshed(data);