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.wemo.internal.handler;
16 import java.time.Instant;
18 import java.util.Map.Entry;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
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;
41 * {@link WemoBaseThingHandler} provides a base implementation for the
42 * concrete WeMo handlers.
44 * @author Jacob Laursen - Initial contribution
47 public abstract class WemoBaseThingHandler extends BaseThingHandler implements UpnpIOParticipant {
49 private static final int SUBSCRIPTION_RENEWAL_INITIAL_DELAY_SECONDS = 15;
50 private static final int SUBSCRIPTION_RENEWAL_INTERVAL_SECONDS = 60;
52 private final Logger logger = LoggerFactory.getLogger(WemoBaseThingHandler.class);
53 private final UpnpIOService service;
54 private final UpnpService upnpService;
56 protected WemoHttpCall wemoHttpCaller;
58 private @Nullable String host;
59 private Map<String, Instant> subscriptions = new ConcurrentHashMap<String, Instant>();
60 private @Nullable ScheduledFuture<?> subscriptionRenewalJob;
62 public WemoBaseThingHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
63 WemoHttpCall wemoHttpCaller) {
65 this.service = upnpIOService;
66 this.upnpService = upnpService;
67 this.wemoHttpCaller = wemoHttpCaller;
71 public void initialize() {
72 logger.debug("Registering UPnP participant for {}", getThing().getUID());
73 service.registerParticipant(this);
78 public void dispose() {
79 removeSubscriptions();
80 logger.debug("Unregistering UPnP participant for {}", getThing().getUID());
81 cancelSubscriptionRenewalJob();
82 service.unregisterParticipant(this);
86 public void handleCommand(ChannelUID channelUID, Command command) {
87 // can be overridden by subclasses
91 public void onStatusChanged(boolean 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.
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);
109 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
110 // can be overridden by subclasses
114 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
115 if (service == null) {
118 logger.debug("Subscription to service {} for {} {}", service, getUDN(), succeeded ? "succeeded" : "failed");
120 subscriptions.put(service, Instant.now());
125 public @Nullable String getUDN() {
126 return (String) this.getConfig().get(WemoBindingConstants.UDN);
129 protected boolean isUpnpDeviceRegistered() {
130 return service.isRegistered(this);
133 protected void addSubscription(String serviceId) {
134 if (subscriptions.containsKey(serviceId)) {
135 logger.debug("{} already subscribed to {}", getUDN(), serviceId);
138 if (subscriptions.isEmpty()) {
139 logger.debug("Adding first GENA subscription for {}, scheduling renewal job", getUDN());
140 scheduleSubscriptionRenewalJob();
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);
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);
154 private void cancelSubscriptionRenewalJob() {
155 ScheduledFuture<?> subscriptionRenewalJob = this.subscriptionRenewalJob;
156 if (subscriptionRenewalJob != null) {
157 subscriptionRenewalJob.cancel(true);
159 this.subscriptionRenewalJob = null;
162 private synchronized void renewSubscriptions() {
163 if (subscriptions.isEmpty()) {
166 if (!service.isRegistered(this)) {
167 logger.debug("Participant not registered when renewing GENA subscriptions for {}, starting UPnP discovery",
169 upnpService.getControlPoint().search(new RootDeviceHeader());
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,
178 service.removeSubscription(this, serviceId);
179 service.addSubscription(this, serviceId, WemoBindingConstants.SUBSCRIPTION_DURATION_SECONDS);
184 private void removeSubscriptions() {
185 if (subscriptions.isEmpty()) {
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);
194 subscriptions.clear();
197 public @Nullable String getWemoURL(String actionService) {
198 String host = getHost();
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
201 "@text/config-status.error.missing-ip");
204 int port = scanForPort(host);
206 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
207 "@text/config-status.error.missing-url");
210 return "http://" + host + ":" + port + "/upnp/control/" + actionService + "1";
213 private @Nullable String getHost() {
221 private void initializeHost() {
222 host = getHostFromService();
225 private int scanForPort(String host) {
226 int portCheckStart = 49151;
227 int portCheckStop = 49157;
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)) {
236 logger.trace("Successfully detected port {}", port);
242 private @Nullable String getHostFromService() {
243 URL descriptorURL = service.getDescriptorURL(this);
244 if (descriptorURL != null) {
245 return descriptorURL.getHost();