/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.io.transport.upnp; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.jupnp.UpnpService; import org.jupnp.controlpoint.ActionCallback; import org.jupnp.controlpoint.ControlPoint; import org.jupnp.controlpoint.SubscriptionCallback; import org.jupnp.model.action.ActionArgumentValue; import org.jupnp.model.action.ActionException; import org.jupnp.model.action.ActionInvocation; import org.jupnp.model.gena.CancelReason; import org.jupnp.model.gena.GENASubscription; import org.jupnp.model.message.UpnpResponse; import org.jupnp.model.meta.Action; import org.jupnp.model.meta.Device; import org.jupnp.model.meta.DeviceIdentity; import org.jupnp.model.meta.RemoteDevice; import org.jupnp.model.meta.Service; import org.jupnp.model.state.StateVariableValue; import org.jupnp.model.types.ServiceId; import org.jupnp.model.types.UDAServiceId; import org.jupnp.model.types.UDN; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link UpnpIOServiceImpl} is the implementation of the UpnpIOService * interface * * @author Karel Goderis - Initial contribution; added simple polling mechanism * @author Kai Kreuzer - added descriptor url retrieval * @author Markus Rathgeb - added NP checks in subscription ended callback * @author Andre Fuechsel - added methods to remove subscriptions * @author Ivan Iliev - made sure resubscribe is only done when subscription ended CancelReason was EXPIRED or * RENEW_FAILED */ @SuppressWarnings("rawtypes") public class UpnpIOServiceImpl implements UpnpIOService { private final Logger logger = LoggerFactory.getLogger(UpnpIOServiceImpl.class); private final int DEFAULT_POLLING_INTERVAL = 60; private UpnpService upnpService; private Map<UpnpIOParticipant, Device> participants = new ConcurrentHashMap<UpnpIOParticipant, Device>(32); private Map<UpnpIOParticipant, ScheduledFuture> pollingJobs = new ConcurrentHashMap<UpnpIOParticipant, ScheduledFuture>( 32); private Map<UpnpIOParticipant, Boolean> currentStates = new ConcurrentHashMap<UpnpIOParticipant, Boolean>(32); private Map<Service, UpnpSubscriptionCallback> subscriptionCallbacks = new ConcurrentHashMap<Service, UpnpSubscriptionCallback>( 32); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5); public class UpnpSubscriptionCallback extends SubscriptionCallback { public UpnpSubscriptionCallback(Service service) { super(service); } public UpnpSubscriptionCallback(Service service, int requestedDurationSeconds) { super(service, requestedDurationSeconds); } @Override protected void ended(GENASubscription subscription, CancelReason reason, UpnpResponse response) { final Service service = subscription.getService(); if (service != null) { final ServiceId serviceId = service.getServiceId(); final Device device = service.getDevice(); if (device != null) { final Device deviceRoot = device.getRoot(); if (deviceRoot != null) { final DeviceIdentity deviceRootIdentity = deviceRoot.getIdentity(); if (deviceRootIdentity != null) { final UDN deviceRootUdn = deviceRootIdentity.getUdn(); logger.debug("A GENA subscription '{}' for device '{}' was ended", serviceId.getId(), deviceRootUdn); } } } if ((CancelReason.EXPIRED.equals(reason) || CancelReason.RENEWAL_FAILED.equals(reason)) && upnpService != null) { final ControlPoint cp = upnpService.getControlPoint(); if (cp != null) { final UpnpSubscriptionCallback callback = new UpnpSubscriptionCallback(service, subscription.getActualDurationSeconds()); cp.execute(callback); } } } } @Override protected void established(GENASubscription subscription) { Device deviceRoot = subscription.getService().getDevice().getRoot(); String serviceId = subscription.getService().getServiceId().getId(); logger.trace("A GENA subscription '{}' for device '{}' is established", serviceId, deviceRoot.getIdentity().getUdn()); for (UpnpIOParticipant participant : participants.keySet()) { if (participants.get(participant).equals(deviceRoot)) { try { participant.onServiceSubscribed(serviceId, true); } catch (Exception e) { logger.error("Participant threw an exception onServiceSubscribed", e); } } } } @SuppressWarnings("unchecked") @Override protected void eventReceived(GENASubscription sub) { Map<String, StateVariableValue> values = sub.getCurrentValues(); Device deviceRoot = sub.getService().getDevice().getRoot(); String serviceId = sub.getService().getServiceId().getId(); logger.trace("Receiving a GENA subscription '{}' response for device '{}'", serviceId, deviceRoot.getIdentity().getUdn()); for (UpnpIOParticipant participant : participants.keySet()) { if (participants.get(participant).equals(deviceRoot)) { for (String stateVariable : values.keySet()) { StateVariableValue value = values.get(stateVariable); if (value.getValue() != null) { try { participant.onValueReceived(stateVariable, value.getValue().toString(), serviceId); } catch (Exception e) { logger.error("Participant threw an exception onValueReceived", e); } } } break; } } } @Override protected void eventsMissed(GENASubscription subscription, int numberOfMissedEvents) { logger.debug("A GENA subscription '{}' for device '{}' missed events", subscription.getService().getServiceId(), subscription.getService().getDevice().getRoot().getIdentity().getUdn()); } @Override protected void failed(GENASubscription subscription, UpnpResponse response, Exception e, String defaultMsg) { Device deviceRoot = subscription.getService().getDevice().getRoot(); String serviceId = subscription.getService().getServiceId().getId(); logger.debug("A GENA subscription '{}' for device '{}' failed", serviceId, deviceRoot.getIdentity().getUdn()); for (UpnpIOParticipant participant : participants.keySet()) { if (participants.get(participant).equals(deviceRoot)) { try { participant.onServiceSubscribed(serviceId, false); } catch (Exception e2) { logger.error("Participant threw an exception onServiceSubscribed", e2); } } } } } public void activate() { logger.debug("Starting UPnP IO service..."); } public void deactivate() { logger.debug("Stopping UPnP IO service..."); } protected void setUpnpService(UpnpService upnpService) { this.upnpService = upnpService; } protected void unsetUpnpService(UpnpService upnpService) { this.upnpService = null; } @Override public void addSubscription(UpnpIOParticipant participant, String serviceID, int duration) { if (participant != null && serviceID != null) { registerParticipant(participant); Device device = participants.get(participant); if (device != null) { Service subService = searchSubService(serviceID, device); if (subService != null) { logger.trace("Setting up an UPNP service subscription '{}' for particpant '{}'", serviceID, participant.getUDN()); UpnpSubscriptionCallback callback = new UpnpSubscriptionCallback(subService, duration); subscriptionCallbacks.put(subService, callback); upnpService.getControlPoint().execute(callback); } else { logger.trace("Could not find service '{}' for device '{}'", serviceID, device.getIdentity().getUdn()); } } else { logger.trace("Could not find an upnp device for participant '{}'", participant.getUDN()); } } } private Service searchSubService(String serviceID, Device device) { Service subService = findService(device, serviceID); if (subService == null) { // service not on the root device, we search the embedded devices as well Device[] embedded = device.getEmbeddedDevices(); if (embedded != null) { for (Device aDevice : embedded) { subService = findService(aDevice, serviceID); if (subService != null) { break; } } } } return subService; } @Override public void removeSubscription(UpnpIOParticipant participant, String serviceID) { if (participant != null && serviceID != null) { Device device = participants.get(participant); if (device != null) { Service subService = searchSubService(serviceID, device); if (subService != null) { logger.trace("Removing an UPNP service subscription '{}' for particpant '{}'", serviceID, participant.getUDN()); UpnpSubscriptionCallback callback = subscriptionCallbacks.get(subService); if (callback != null) { callback.end(); } subscriptionCallbacks.remove(subService); } else { logger.trace("Could not find service '{}' for device '{}'", serviceID, device.getIdentity().getUdn()); } } else { logger.trace("Could not find an upnp device for participant '{}'", participant.getUDN()); } } } @Override @SuppressWarnings("unchecked") public Map<String, String> invokeAction(UpnpIOParticipant participant, String serviceID, String actionID, Map<String, String> inputs) { HashMap<String, String> resultMap = new HashMap<String, String>(); if (serviceID != null && actionID != null && participant != null) { registerParticipant(participant); Device device = participants.get(participant); if (device != null) { Service service = findService(device, serviceID); if (service != null) { Action action = service.getAction(actionID); if (action != null) { ActionInvocation invocation = new ActionInvocation(action); if (invocation != null) { if (inputs != null) { for (String variable : inputs.keySet()) { invocation.setInput(variable, inputs.get(variable)); } } logger.trace("Invoking Action '{}' of service '{}' for participant '{}'", new Object[] { actionID, serviceID, participant.getUDN() }); new ActionCallback.Default(invocation, upnpService.getControlPoint()).run(); ActionException anException = invocation.getFailure(); if (anException != null && anException.getMessage() != null) { logger.debug(anException.getMessage()); } Map<String, ActionArgumentValue> result = invocation.getOutputMap(); if (result != null) { for (String variable : result.keySet()) { final ActionArgumentValue newArgument; try { newArgument = result.get(variable); } catch (final Exception ex) { logger.debug( "An exception '{}' occurred, cannot get argument for variable '{}'", ex.getMessage(), variable); continue; } try { if (newArgument.getValue() != null) { resultMap.put(variable, newArgument.getValue().toString()); } } catch (final Exception ex) { logger.debug( "An exception '{}' occurred processing ActionArgumentValue '{}' with value '{}'", new Object[] { ex.getMessage(), newArgument.getArgument().getName(), newArgument.getValue() }); } } } } } else { logger.debug("Could not find action '{}' for participant '{}'", actionID, participant.getUDN()); } } else { logger.debug("Could not find service '{}' for participant '{}'", serviceID, participant.getUDN()); } } else { logger.debug("Could not find an upnp device for participant '{}'", participant.getUDN()); } } return resultMap; } @Override public boolean isRegistered(UpnpIOParticipant participant) { if (upnpService.getRegistry().getDevice(new UDN(participant.getUDN()), true) != null) { return true; } else { return false; } } @Override public void registerParticipant(UpnpIOParticipant participant) { if (participant != null) { Device device = participants.get(participant); if (device == null) { device = upnpService.getRegistry().getDevice(new UDN(participant.getUDN()), true); if (device != null) { logger.debug("Registering device '{}' for participant '{}'", device.getIdentity(), participant.getUDN()); participants.put(participant, device); } } } } @Override public void unregisterParticipant(UpnpIOParticipant participant) { if (participant != null) { stopPollingForParticipant(participant); pollingJobs.remove(participant); currentStates.remove(participant); participants.remove(participant); } } @Override public URL getDescriptorURL(UpnpIOParticipant participant) { RemoteDevice device = upnpService.getRegistry().getRemoteDevice(new UDN(participant.getUDN()), true); if (device != null) { return device.getIdentity().getDescriptorURL(); } else { return null; } } private Service findService(Device device, String serviceID) { Service service = null; String namespace = device.getType().getNamespace(); if (namespace.equals(UDAServiceId.DEFAULT_NAMESPACE) || namespace.equals(UDAServiceId.BROKEN_DEFAULT_NAMESPACE)) { service = device.findService(new UDAServiceId(serviceID)); } else { service = device.findService(new ServiceId(namespace, serviceID)); } return service; } private class UPNPPollingRunnable implements Runnable { private UpnpIOParticipant participant; private String serviceID; private String actionID; public UPNPPollingRunnable(UpnpIOParticipant participant, String serviceID, String actionID) { this.participant = participant; this.serviceID = serviceID; this.actionID = actionID; } @Override public void run() { // It is assumed that during addStatusListener() a check is made // whether the participant // is correctly registered try { Device device = participants.get(participant); if (device != null) { Service service = findService(device, serviceID); if (service != null) { Action action = service.getAction(actionID); if (action != null) { @SuppressWarnings("unchecked") ActionInvocation invocation = new ActionInvocation(action); if (invocation != null) { logger.debug("Polling participant '{}' through Action '{}' of Service '{}' ", new Object[] { participant.getUDN(), actionID, serviceID }); new ActionCallback.Default(invocation, upnpService.getControlPoint()).run(); ActionException anException = invocation.getFailure(); if (anException != null && anException.getMessage() .contains("Connection error or no response received")) { // The UDN is not reachable anymore if (currentStates.get(participant)) { currentStates.put(participant, false); logger.debug("Signalling that '{}' is not responding", participant.getUDN()); participant.onStatusChanged(false); } } else { // The UDN functions correctly if (!currentStates.get(participant)) { currentStates.put(participant, true); logger.debug("Signalling that '{}' is again responding", participant.getUDN()); participant.onStatusChanged(true); } } } } else { logger.debug("Could not find action '{}' for participant '{}'", actionID, participant.getUDN()); } } else { logger.debug("Could not find service '{}' for participant '{}'", serviceID, participant.getUDN()); } } } catch (Exception e) { logger.error("An exception occurred while polling an UPNP device: '{}'", e.getStackTrace().toString()); } } } @Override public void addStatusListener(UpnpIOParticipant participant, String serviceID, String actionID, int interval) { if (participant != null) { registerParticipant(participant); int pollingInterval = interval == 0 ? DEFAULT_POLLING_INTERVAL : interval; // remove the previous polling job, if any stopPollingForParticipant(participant); currentStates.put(participant, true); Runnable pollingRunnable = new UPNPPollingRunnable(participant, serviceID, actionID); pollingJobs.put(participant, scheduler.scheduleAtFixedRate(pollingRunnable, 0, pollingInterval, TimeUnit.SECONDS)); } } private void stopPollingForParticipant(UpnpIOParticipant participant) { if (pollingJobs.containsKey(participant)) { ScheduledFuture<?> pollingJob = pollingJobs.get(participant); if (pollingJob != null) { pollingJob.cancel(true); } } } @Override public void removeStatusListener(UpnpIOParticipant participant) { if (participant != null) { unregisterParticipant(participant); } } }