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