]> git.basschouten.com Git - openhab-addons.git/blob
d1a45e76291bd64026b4f50081362bf3b4389ce4
[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.wemo.internal.handler;
14
15 import java.net.URL;
16 import java.time.Instant;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
25 import java.util.stream.IntStream;
26 import java.util.stream.Stream;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.jupnp.UpnpService;
31 import org.jupnp.model.message.header.RootDeviceHeader;
32 import org.openhab.binding.wemo.internal.WemoBindingConstants;
33 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
34 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
35 import org.openhab.core.io.transport.upnp.UpnpIOService;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.types.Command;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * {@link WemoBaseThingHandler} provides a base implementation for the
47  * concrete WeMo handlers.
48  *
49  * @author Jacob Laursen - Initial contribution
50  */
51 @NonNullByDefault
52 public abstract class WemoBaseThingHandler extends BaseThingHandler implements UpnpIOParticipant {
53
54     private static final int SUBSCRIPTION_RENEWAL_INITIAL_DELAY_SECONDS = 15;
55     private static final int SUBSCRIPTION_RENEWAL_INTERVAL_SECONDS = 60;
56     private static final int PORT_RANGE_START = 49151;
57     private static final int PORT_RANGE_END = 49157;
58
59     private final Logger logger = LoggerFactory.getLogger(WemoBaseThingHandler.class);
60     private final UpnpIOService service;
61     private final UpnpService upnpService;
62
63     protected WemoHttpCall wemoHttpCaller;
64
65     private @Nullable String host;
66     private Map<String, Instant> subscriptions = new ConcurrentHashMap<String, Instant>();
67     private @Nullable ScheduledFuture<?> subscriptionRenewalJob;
68
69     public WemoBaseThingHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
70             WemoHttpCall wemoHttpCaller) {
71         super(thing);
72         this.service = upnpIOService;
73         this.upnpService = upnpService;
74         this.wemoHttpCaller = wemoHttpCaller;
75     }
76
77     @Override
78     public void initialize() {
79         logger.debug("Registering UPnP participant for {}", getThing().getUID());
80         service.registerParticipant(this);
81         initializeHost();
82     }
83
84     @Override
85     public void dispose() {
86         cancelSubscriptionRenewalJob();
87         removeSubscriptions();
88         logger.debug("Unregistering UPnP participant for {}", getThing().getUID());
89         service.unregisterParticipant(this);
90     }
91
92     @Override
93     public void handleCommand(ChannelUID channelUID, Command command) {
94         // can be overridden by subclasses
95     }
96
97     @Override
98     public void onStatusChanged(boolean status) {
99         if (status) {
100             logger.debug("UPnP device {} for {} is present", getUDN(), getThing().getUID());
101             if (service.isRegistered(this)) {
102                 // After successful discovery, try to subscribe again.
103                 renewSubscriptions();
104             }
105         } else {
106             logger.info("UPnP device {} for {} is absent", getUDN(), getThing().getUID());
107             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR);
108             // Expire subscriptions.
109             for (Entry<String, Instant> subscription : subscriptions.entrySet()) {
110                 subscription.setValue(Instant.MIN);
111             }
112         }
113     }
114
115     @Override
116     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
117         // can be overridden by subclasses
118     }
119
120     @Override
121     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
122         if (service == null) {
123             return;
124         }
125         logger.debug("Subscription to service {} for {} {}", service, getUDN(), succeeded ? "succeeded" : "failed");
126         if (succeeded) {
127             subscriptions.put(service, Instant.now());
128         }
129     }
130
131     @Override
132     public @Nullable String getUDN() {
133         return (String) this.getConfig().get(WemoBindingConstants.UDN);
134     }
135
136     protected boolean isUpnpDeviceRegistered() {
137         return service.isRegistered(this);
138     }
139
140     protected void addSubscription(String serviceId) {
141         if (subscriptions.containsKey(serviceId)) {
142             logger.debug("{} already subscribed to {}", getUDN(), serviceId);
143             return;
144         }
145         if (subscriptions.isEmpty()) {
146             logger.debug("Adding first GENA subscription for {}, scheduling renewal job", getUDN());
147             scheduleSubscriptionRenewalJob();
148         }
149         subscriptions.put(serviceId, Instant.MIN);
150         logger.debug("Adding GENA subscription {} for {}, participant is {}", serviceId, getUDN(),
151                 service.isRegistered(this) ? "registered" : "not registered");
152         service.addSubscription(this, serviceId, WemoBindingConstants.SUBSCRIPTION_DURATION_SECONDS);
153     }
154
155     private void scheduleSubscriptionRenewalJob() {
156         cancelSubscriptionRenewalJob();
157         this.subscriptionRenewalJob = scheduler.scheduleWithFixedDelay(this::renewSubscriptions,
158                 SUBSCRIPTION_RENEWAL_INITIAL_DELAY_SECONDS, SUBSCRIPTION_RENEWAL_INTERVAL_SECONDS, TimeUnit.SECONDS);
159     }
160
161     private void cancelSubscriptionRenewalJob() {
162         ScheduledFuture<?> subscriptionRenewalJob = this.subscriptionRenewalJob;
163         if (subscriptionRenewalJob != null) {
164             subscriptionRenewalJob.cancel(true);
165         }
166         this.subscriptionRenewalJob = null;
167     }
168
169     private synchronized void renewSubscriptions() {
170         if (subscriptions.isEmpty()) {
171             return;
172         }
173         if (!service.isRegistered(this)) {
174             logger.debug("Participant not registered when renewing GENA subscriptions for {}, starting UPnP discovery",
175                     getUDN());
176             upnpService.getControlPoint().search(new RootDeviceHeader());
177             return;
178         }
179         logger.debug("Renewing GENA subscriptions for {}", getUDN());
180         subscriptions.forEach((serviceId, lastRenewed) -> {
181             if (lastRenewed.isBefore(Instant.now().minusSeconds(
182                     WemoBindingConstants.SUBSCRIPTION_DURATION_SECONDS - SUBSCRIPTION_RENEWAL_INTERVAL_SECONDS))) {
183                 logger.debug("Subscription for service {} with timestamp {} has expired, renewing", serviceId,
184                         lastRenewed);
185                 service.removeSubscription(this, serviceId);
186                 service.addSubscription(this, serviceId, WemoBindingConstants.SUBSCRIPTION_DURATION_SECONDS);
187             }
188         });
189     }
190
191     private void removeSubscriptions() {
192         if (subscriptions.isEmpty()) {
193             return;
194         }
195         logger.debug("Removing GENA subscriptions for {}, participant is {}", getUDN(),
196                 service.isRegistered(this) ? "registered" : "not registered");
197         subscriptions.forEach((serviceId, lastRenewed) -> {
198             logger.debug("Removing subscription for service {}", serviceId);
199             service.removeSubscription(this, serviceId);
200         });
201         subscriptions.clear();
202     }
203
204     public @Nullable String getWemoURL(String actionService) {
205         String host = getHost();
206         if (host == null) {
207             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
208                     "@text/config-status.error.missing-ip");
209             return null;
210         }
211         int port = scanForPort(host);
212         if (port == 0) {
213             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
214                     "@text/config-status.error.missing-url");
215             return null;
216         }
217         return "http://" + host + ":" + port + "/upnp/control/" + actionService + "1";
218     }
219
220     private @Nullable String getHost() {
221         if (host != null) {
222             return host;
223         }
224         initializeHost();
225         return host;
226     }
227
228     private void initializeHost() {
229         host = getHostFromService();
230     }
231
232     private int scanForPort(String host) {
233         Integer portFromService = getPortFromService();
234         List<Integer> portsToCheck = new ArrayList<Integer>(PORT_RANGE_END - PORT_RANGE_START + 1);
235         Stream<Integer> portRange = IntStream.rangeClosed(PORT_RANGE_START, PORT_RANGE_END).boxed();
236         if (portFromService != null) {
237             portsToCheck.add(portFromService);
238             portRange = portRange.filter(p -> p.intValue() != portFromService);
239         }
240         portsToCheck.addAll(portRange.collect(Collectors.toList()));
241         int port = 0;
242         for (Integer portCheck : portsToCheck) {
243             String urlProbe = "http://" + host + ":" + portCheck;
244             logger.trace("Probing {} to find port", urlProbe);
245             if (!wemoHttpCaller.probeURL(urlProbe)) {
246                 continue;
247             }
248             port = portCheck;
249             logger.trace("Successfully detected port {}", port);
250             break;
251         }
252         return port;
253     }
254
255     private @Nullable String getHostFromService() {
256         URL descriptorURL = service.getDescriptorURL(this);
257         if (descriptorURL != null) {
258             return descriptorURL.getHost();
259         }
260         return null;
261     }
262
263     private @Nullable Integer getPortFromService() {
264         URL descriptorURL = service.getDescriptorURL(this);
265         if (descriptorURL != null) {
266             int port = descriptorURL.getPort();
267             if (port != -1) {
268                 return port;
269             }
270         }
271         return null;
272     }
273 }