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;
17 import java.util.ArrayList;
18 import java.util.List;
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;
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;
46 * {@link WemoBaseThingHandler} provides a base implementation for the
47 * concrete WeMo handlers.
49 * @author Jacob Laursen - Initial contribution
52 public abstract class WemoBaseThingHandler extends BaseThingHandler implements UpnpIOParticipant {
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;
59 private final Logger logger = LoggerFactory.getLogger(WemoBaseThingHandler.class);
60 private final UpnpIOService service;
61 private final UpnpService upnpService;
63 protected WemoHttpCall wemoHttpCaller;
65 private @Nullable String host;
66 private Map<String, Instant> subscriptions = new ConcurrentHashMap<String, Instant>();
67 private @Nullable ScheduledFuture<?> subscriptionRenewalJob;
69 public WemoBaseThingHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
70 WemoHttpCall wemoHttpCaller) {
72 this.service = upnpIOService;
73 this.upnpService = upnpService;
74 this.wemoHttpCaller = wemoHttpCaller;
78 public void initialize() {
79 logger.debug("Registering UPnP participant for {}", getThing().getUID());
80 service.registerParticipant(this);
85 public void dispose() {
86 cancelSubscriptionRenewalJob();
87 removeSubscriptions();
88 logger.debug("Unregistering UPnP participant for {}", getThing().getUID());
89 service.unregisterParticipant(this);
93 public void handleCommand(ChannelUID channelUID, Command command) {
94 // can be overridden by subclasses
98 public void onStatusChanged(boolean 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();
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);
116 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
117 // can be overridden by subclasses
121 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
122 if (service == null) {
125 logger.debug("Subscription to service {} for {} {}", service, getUDN(), succeeded ? "succeeded" : "failed");
127 subscriptions.put(service, Instant.now());
132 public @Nullable String getUDN() {
133 return (String) this.getConfig().get(WemoBindingConstants.UDN);
136 protected boolean isUpnpDeviceRegistered() {
137 return service.isRegistered(this);
140 protected void addSubscription(String serviceId) {
141 if (subscriptions.containsKey(serviceId)) {
142 logger.debug("{} already subscribed to {}", getUDN(), serviceId);
145 if (subscriptions.isEmpty()) {
146 logger.debug("Adding first GENA subscription for {}, scheduling renewal job", getUDN());
147 scheduleSubscriptionRenewalJob();
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);
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);
161 private void cancelSubscriptionRenewalJob() {
162 ScheduledFuture<?> subscriptionRenewalJob = this.subscriptionRenewalJob;
163 if (subscriptionRenewalJob != null) {
164 subscriptionRenewalJob.cancel(true);
166 this.subscriptionRenewalJob = null;
169 private synchronized void renewSubscriptions() {
170 if (subscriptions.isEmpty()) {
173 if (!service.isRegistered(this)) {
174 logger.debug("Participant not registered when renewing GENA subscriptions for {}, starting UPnP discovery",
176 upnpService.getControlPoint().search(new RootDeviceHeader());
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,
185 service.removeSubscription(this, serviceId);
186 service.addSubscription(this, serviceId, WemoBindingConstants.SUBSCRIPTION_DURATION_SECONDS);
191 private void removeSubscriptions() {
192 if (subscriptions.isEmpty()) {
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);
201 subscriptions.clear();
204 public @Nullable String getWemoURL(String actionService) {
205 String host = getHost();
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
208 "@text/config-status.error.missing-ip");
211 int port = scanForPort(host);
213 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
214 "@text/config-status.error.missing-url");
217 return "http://" + host + ":" + port + "/upnp/control/" + actionService + "1";
220 private @Nullable String getHost() {
228 private void initializeHost() {
229 host = getHostFromService();
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);
240 portsToCheck.addAll(portRange.collect(Collectors.toList()));
242 for (Integer portCheck : portsToCheck) {
243 String urlProbe = "http://" + host + ":" + portCheck;
244 logger.trace("Probing {} to find port", urlProbe);
245 if (!wemoHttpCaller.probeURL(urlProbe)) {
249 logger.trace("Successfully detected port {}", port);
255 private @Nullable String getHostFromService() {
256 URL descriptorURL = service.getDescriptorURL(this);
257 if (descriptorURL != null) {
258 return descriptorURL.getHost();
263 private @Nullable Integer getPortFromService() {
264 URL descriptorURL = service.getDescriptorURL(this);
265 if (descriptorURL != null) {
266 int port = descriptorURL.getPort();