2 * Copyright (c) 2010-2022 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.webhook;
15 import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.BINDING_ID;
17 import java.io.IOException;
18 import java.io.InputStream;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.List;
24 import java.util.Optional;
25 import java.util.Scanner;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.stream.Collectors;
29 import javax.servlet.ServletException;
30 import javax.servlet.http.HttpServlet;
31 import javax.servlet.http.HttpServletRequest;
32 import javax.servlet.http.HttpServletResponse;
33 import javax.ws.rs.HttpMethod;
34 import javax.ws.rs.core.MediaType;
35 import javax.ws.rs.core.UriBuilder;
36 import javax.ws.rs.core.UriBuilderException;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.netatmo.internal.api.NetatmoException;
41 import org.openhab.binding.netatmo.internal.api.SecurityApi;
42 import org.openhab.binding.netatmo.internal.api.dto.WebhookEvent;
43 import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
44 import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
45 import org.openhab.binding.netatmo.internal.handler.capability.EventCapability;
46 import org.osgi.service.http.HttpService;
47 import org.osgi.service.http.NamespaceException;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * HTTP servlet for Netatmo Webhook.
54 * @author Gaƫl L'hopital - Initial contribution
57 public class NetatmoServlet extends HttpServlet {
58 private static final long serialVersionUID = -354583910860541214L;
59 private static final String CALLBACK_URI = "/" + BINDING_ID;
61 private final Logger logger = LoggerFactory.getLogger(NetatmoServlet.class);
62 private final Map<String, EventCapability> dataListeners = new ConcurrentHashMap<>();
63 private final HttpService httpService;
64 private final NADeserializer deserializer;
65 private final Optional<SecurityApi> securityApi;
66 private boolean hookSet = false;
68 public NetatmoServlet(HttpService httpService, ApiBridgeHandler apiBridge, String webHookUrl) {
69 this.httpService = httpService;
70 this.deserializer = apiBridge.getDeserializer();
71 this.securityApi = Optional.ofNullable(apiBridge.getRestManager(SecurityApi.class));
72 securityApi.ifPresent(api -> {
74 httpService.registerServlet(CALLBACK_URI, this, null, httpService.createDefaultHttpContext());
75 logger.debug("Started Netatmo Webhook Servlet at '{}'", CALLBACK_URI);
76 URI uri = UriBuilder.fromUri(webHookUrl).path(BINDING_ID).build();
78 logger.info("Setting Netatmo Welcome WebHook to {}", uri.toString());
81 } catch (UriBuilderException e) {
82 logger.info("webhookUrl is not a valid URI '{}' : {}", uri, e.getMessage());
83 } catch (NetatmoException e) {
84 logger.info("Error setting webhook : {}", e.getMessage());
86 } catch (ServletException | NamespaceException e) {
87 logger.warn("Could not start Netatmo Webhook Servlet : {}", e.getMessage());
92 public void dispose() {
93 securityApi.ifPresent(api -> {
95 logger.info("Releasing Netatmo Welcome WebHook");
98 } catch (NetatmoException e) {
99 logger.warn("Error releasing webhook : {}", e.getMessage());
102 httpService.unregister(CALLBACK_URI);
104 logger.debug("Netatmo Webhook Servlet stopped");
108 protected void service(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
109 if (req != null && resp != null) {
110 String data = inputStreamToString(req.getInputStream());
111 if (!data.isEmpty()) {
112 logger.debug("Event transmitted from restService : {}", data);
114 WebhookEvent event = deserializer.deserialize(WebhookEvent.class, data);
115 List<String> tobeNotified = collectNotified(event);
116 dataListeners.keySet().stream().filter(tobeNotified::contains).forEach(id -> {
117 EventCapability module = dataListeners.get(id);
118 if (module != null) {
119 module.setNewData(event);
122 } catch (NetatmoException e) {
123 logger.info("Error deserializing webhook data received : {}. {}", data, e.getMessage());
126 resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
127 resp.setContentType(MediaType.APPLICATION_JSON);
128 resp.setHeader("Access-Control-Allow-Origin", "*");
129 resp.setHeader("Access-Control-Allow-Methods", HttpMethod.POST);
130 resp.setIntHeader("Access-Control-Max-Age", 3600);
131 resp.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
132 resp.getWriter().write("");
136 private List<String> collectNotified(WebhookEvent event) {
137 List<String> result = new ArrayList<>();
138 result.add(event.getCameraId());
139 String person = event.getPersonId();
140 if (person != null) {
143 result.addAll(event.getPersons().keySet());
144 return result.stream().distinct().collect(Collectors.toList());
147 public void registerDataListener(String id, EventCapability dataListener) {
148 dataListeners.put(id, dataListener);
151 public void unregisterDataListener(EventCapability dataListener) {
152 dataListeners.entrySet().removeIf(entry -> entry.getValue().equals(dataListener));
155 private String inputStreamToString(InputStream is) throws IOException {
157 try (Scanner scanner = new Scanner(is)) {
158 scanner.useDelimiter("\\A");
159 value = scanner.hasNext() ? scanner.next() : "";