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.concurrent.ConcurrentHashMap;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
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;
39 * {@link WemoBaseThingHandler} provides a base implementation for the
40 * concrete WeMo handlers.
42 * @author Jacob Laursen - Initial contribution
45 public abstract class WemoBaseThingHandler extends BaseThingHandler implements UpnpIOParticipant {
47 private static final int SUBSCRIPTION_RENEWAL_INITIAL_DELAY_SECONDS = 15;
48 private static final int SUBSCRIPTION_RENEWAL_INTERVAL_SECONDS = 60;
50 private final Logger logger = LoggerFactory.getLogger(WemoBaseThingHandler.class);
52 protected @Nullable UpnpIOService service;
53 protected WemoHttpCall wemoHttpCaller;
55 private @Nullable String host;
56 private Map<String, Instant> subscriptions = new ConcurrentHashMap<String, Instant>();
57 private @Nullable ScheduledFuture<?> subscriptionRenewalJob;
59 public WemoBaseThingHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
61 this.service = upnpIOService;
62 this.wemoHttpCaller = wemoHttpCaller;
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);
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);
83 cancelSubscriptionRenewalJob();
87 public void handleCommand(ChannelUID channelUID, Command command) {
88 // can be overridden by subclasses
92 public void onStatusChanged(boolean status) {
93 // can be overridden by subclasses
97 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
98 // can be overridden by subclasses
102 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
103 if (service == null) {
106 logger.debug("Subscription to service {} for {} {}", service, getUDN(), succeeded ? "succeeded" : "failed");
108 subscriptions.put(service, Instant.now());
113 public @Nullable String getUDN() {
114 return (String) this.getThing().getConfiguration().get(WemoBindingConstants.UDN);
117 protected boolean isUpnpDeviceRegistered() {
118 UpnpIOService service = this.service;
119 return service != null && service.isRegistered(this);
122 protected void addSubscription(String serviceId) {
123 if (subscriptions.containsKey(serviceId)) {
124 logger.debug("{} already subscribed to {}", getUDN(), serviceId);
127 if (subscriptions.isEmpty()) {
128 logger.debug("Adding first GENA subscription for {}, scheduling renewal job", getUDN());
129 scheduleSubscriptionRenewalJob();
131 subscriptions.put(serviceId, Instant.ofEpochSecond(0));
132 UpnpIOService service = this.service;
133 if (service == null) {
136 if (!service.isRegistered(this)) {
137 logger.debug("Registering UPnP participant for {}", getUDN());
138 service.registerParticipant(this);
140 if (!service.isRegistered(this)) {
141 logger.debug("Trying to add GENA subscription {} for {}, but service is not registered", serviceId,
145 logger.debug("Adding GENA subscription {} for {}", serviceId, getUDN());
146 service.addSubscription(this, serviceId, WemoBindingConstants.SUBSCRIPTION_DURATION_SECONDS);
149 protected void removeSubscription(String serviceId) {
150 UpnpIOService service = this.service;
151 if (service == null) {
154 subscriptions.remove(serviceId);
155 if (subscriptions.isEmpty()) {
156 logger.debug("Removing last GENA subscription for {}, cancelling renewal job", getUDN());
157 cancelSubscriptionRenewalJob();
159 if (!service.isRegistered(this)) {
160 logger.debug("Trying to remove GENA subscription {} for {}, but service is not registered", serviceId,
164 logger.debug("Unsubscribing {} from service {}", getUDN(), serviceId);
165 service.removeSubscription(this, serviceId);
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);
174 private void cancelSubscriptionRenewalJob() {
175 ScheduledFuture<?> subscriptionRenewalJob = this.subscriptionRenewalJob;
176 if (subscriptionRenewalJob != null) {
177 subscriptionRenewalJob.cancel(true);
179 this.subscriptionRenewalJob = null;
182 private void renewSubscriptions() {
183 if (subscriptions.isEmpty()) {
186 UpnpIOService service = this.service;
187 if (service == null) {
190 if (!service.isRegistered(this)) {
191 service.registerParticipant(this);
193 if (!service.isRegistered(this)) {
194 logger.debug("Trying to renew GENA subscriptions for {}, but service is not registered", getUDN());
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,
203 service.removeSubscription(this, serviceId);
204 service.addSubscription(this, serviceId, WemoBindingConstants.SUBSCRIPTION_DURATION_SECONDS);
209 private void removeSubscriptions() {
210 if (subscriptions.isEmpty()) {
213 UpnpIOService service = this.service;
214 if (service == null) {
217 if (!service.isRegistered(this)) {
218 logger.debug("Trying to remove GENA subscriptions for {}, but service is not registered",
219 getThing().getUID());
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);
227 subscriptions.clear();
230 public @Nullable String getWemoURL(String actionService) {
231 String host = getHost();
233 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
234 "@text/config-status.error.missing-ip");
237 int portCheckStart = 49151;
238 int portCheckStop = 49157;
240 for (int i = portCheckStart; i < portCheckStop; i++) {
241 if (WemoUtil.serviceAvailableFunction.apply(host, i)) {
242 port = String.valueOf(i);
247 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248 "@text/config-status.error.missing-url");
251 return "http://" + host + ":" + port + "/upnp/control/" + actionService + "1";
254 private @Nullable String getHost() {
262 private void initializeHost() {
263 host = getHostFromService();
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();