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