/* * Copyright Ericsson AB 2011-2014. All Rights Reserved. * * The contents of this file are subject to the Lesser GNU Public License, * (the "License"), either version 2.1 of the License, or * (at your option) any later version.; you may not use this file except in * compliance with the License. You should have received a copy of the * License along with this software. If not, it can be * retrieved online at https://www.gnu.org/licenses/lgpl.html. Moreover * it could also be requested from Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO * WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. * EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR * OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, * EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE * LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, * YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. * * IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING * WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR * REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR * DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL * DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY * (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED * INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE * OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH * HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. * */ package com.ericsson.deviceaccess.upnp; import com.ericsson.common.util.LegacyUtil; import com.ericsson.common.util.StringUtil; import com.ericsson.common.util.function.FunctionalUtil; import com.ericsson.deviceaccess.api.Constants; import com.ericsson.deviceaccess.api.GenericDevice; import com.ericsson.deviceaccess.api.GenericDevice.State; import com.ericsson.deviceaccess.api.genericdevice.GDProperties; import com.ericsson.deviceaccess.api.genericdevice.GDService; import com.ericsson.deviceaccess.spi.schema.based.SBGenericDevice; import java.util.Dictionary; import java.util.HashMap; import java.util.Map; import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.osgi.framework.BundleContext; import org.osgi.framework.Filter; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceRegistration; import org.osgi.service.upnp.UPnPDevice; import org.osgi.service.upnp.UPnPEventListener; import org.osgi.service.upnp.UPnPService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class UPnPDeviceAgent implements UPnPEventListener { private static final Logger logger = LoggerFactory.getLogger(UPnPDeviceAgent.class); private static final Pattern TITLE_PATTERN = Pattern.compile("dc:title[>>]+([^&]*)[&<lt;]+/dc:title"); private static final Map<String, BiConsumer<GDProperties, String>> eventTypes = new HashMap<>(); private static final BiConsumer<GDProperties, String> EMPTY = (a, b) -> { }; static { eventTypes.put("Volume", (properties, value) -> { try { properties.setStringValue("CurrentVolume", value); } catch (Exception e) { // TODO: Parsing error, it seems the string contains channel as well } }); // TODO: This does not work with the Noxon. It uses a different variable for this information eventTypes.put("AVTransportURI", (properties, value) -> properties.setStringValue("CurrentUrl", value)); // TODO: This does not work with the Noxon. It uses a different variable for this information eventTypes.put("CurrentTrackMetaData", (properties, value) -> { String title = getMediaTitle(value); logger.debug("Media title is " + title); properties.setStringValue("CurrentTitle", title); }); eventTypes.put("TransportState", (properties, value) -> { String state = value.toLowerCase(); switch (state) { case "playing": case "stopped": case "paused": properties.setStringValue("Status", StringUtil.capitalize(state)); } }); } protected static String getMediaTitle(String didl) { Matcher matcher = TITLE_PATTERN.matcher(didl); if (matcher.find()) { return matcher.group(1); } return ""; } private final BundleContext context; private final SBGenericDevice device; private ServiceRegistration eventListenerReg; private ServiceRegistration devReg; final private Map<String, GDService> idToService = new HashMap<>(); public UPnPDeviceAgent(BundleContext bc, UPnPDevice upnpdev) { this.context = bc; device = new SBGenericDevice() { }; device.setId(UPnPUtil.getUDN(upnpdev)); device.setOnline(true); device.setName(UPnPUtil.getFriendlyName(upnpdev)); device.setProtocol("upnp"); device.setType(UPnPUtil.getDeviceType(upnpdev)); device.setURN((String) upnpdev.getDescriptions(null).get(UPnPDevice.UDN)); device.setManufacturer((String) upnpdev.getDescriptions(null).get(UPnPDevice.MANUFACTURER)); device.setSerialNumber((String) upnpdev.getDescriptions(null).get(UPnPDevice.SERIAL_NUMBER)); device.setModelName((String) upnpdev.getDescriptions(null).get(UPnPDevice.MODEL_NAME)); String productClass = (String) upnpdev.getDescriptions(null).get(UPnPDevice.UPC); if (productClass == null) { productClass = ""; } else { productClass = productClass.trim(); } device.setProductClass(productClass); device.setService(getServices(upnpdev)); String iconUrl = (String) upnpdev.getDescriptions(null).get("GDA_ICON"); device.setIcon(iconUrl); } public void update() { } public void start() { subscribeToEvents(UPnPFilterRule.deviceID(device.getId())); device.setState(State.ADDED); devReg = context.registerService(GenericDevice.class, device, LegacyUtil.toDictionary(device.getDeviceProperties())); device.setState(State.READY); } public void stop() { unsubscribeFromEvents(); device.setOnline(false); device.setState(State.REMOVED); if (devReg != null) { devReg.unregister(); devReg = null; } } private void unsubscribeFromEvents() { if (this.eventListenerReg != null) { logger.debug("Unsubscribing from UPnP events"); eventListenerReg.unregister(); eventListenerReg = null; } } private void subscribeToEvents(UPnPFilterRule rule) { logger.debug("Subscribing to UPnP events"); if (rule != null) { try { Filter filter = context.createFilter(rule.toFilterRule()); Map<String, Object> props = new HashMap<>(); props.put(UPnPEventListener.UPNP_FILTER, filter); this.eventListenerReg = context.registerService(UPnPEventListener.class, this, LegacyUtil.toDictionary(props)); } catch (InvalidSyntaxException e) { logger.error("Parsing failed: " + e); } } } private Map<String, GDService> getServices(UPnPDevice dev) { HashMap<String, GDService> services = new HashMap<>(); GDService service = null; if (UPnPUtil.isMediaRenderer(dev)) { logger.debug("Media Renderer is found"); service = new RenderingControlUPnPImpl(dev); } else if (UPnPUtil.isMediaServer(dev)) { logger.debug("Media Server is found"); service = new ContentDirectoryUPnPImpl(dev); } else if (UPnPUtil.isDimmableLight(dev)) { UPnPService[] upnpServices = dev.getServices(); logger.debug("Dimmable Light is found"); for (UPnPService upnpService : upnpServices) { String serviceType = upnpService.getType(); String[] serviceTypeParts = UPnPUtil.parseServiceType(serviceType); if (serviceTypeParts == null || serviceTypeParts.length < 4) { logger.debug("Unformatted service type: " + serviceType); continue; } String type = serviceTypeParts[3]; logger.debug("Serivce Type " + type); if (type != null) { switch (type) { case "DimmingService": service = new DimmingUPnPImpl(dev, upnpService, logger); break; case "SwitchPower": service = new SwitchPowerUPnPImpl(dev, upnpService, logger); break; default: logger.debug("Unexpected service type: " + serviceType); break; } if (service != null) { idToService.put(upnpService.getId(), service); } } } } if (service != null) { services.put(service.getName(), service); } return services; } @Override public void notifyUPnPEvent(String deviceId, String serviceId, Dictionary eventTable) { Map<String, String> events = LegacyUtil.toMap(eventTable); logger.debug("UPnP event received for " + deviceId + "#" + serviceId); if (deviceId.equals(device.getId())) { GDService svc = device.getService(getSWoTServiceNameFromUPnPServiceId(serviceId)); if (svc != null) { events.forEach((event, data) -> { if (event.equals("LastChange")) { logger.debug("Received LastChange variables event"); UPnPUtil.parseLastChangeEvent(data).forEach((name, value) -> { eventTypes.getOrDefault(name, EMPTY).accept(svc.getProperties(), value); }); notifyUpdate(svc.getPath(true) + "/parameter"); } }); } /* * Update properties of each service */ FunctionalUtil.acceptIfCan(UpdatePropertyInterface.class, idToService.get(serviceId), service -> { logger.debug("Found UpdatePropertyInterface instance"); events.forEach((name, value) -> { service.updateProperty(name, value); /* logger.debug("Event: " + event + " = " + eventValue); if ("LastChange".equals(event)) { logger.debug("Received LastChange variables event"); Properties changedVars = UPnPUtil.parseLastChangeEvent((String)eventTable.get(event)); for (Enumeration vars = changedVars.keys(); vars.hasMoreElements();) { String name = (String)vars.nextElement(); String value = (String)changedVars.getProperty(name); } } */ }); }); } } private void notifyUpdate(String path) { if (devReg != null) { Map<String, Object> props = device.getDeviceProperties(); props.put(Constants.UPDATED_PATH, path); devReg.setProperties(LegacyUtil.toDictionary(props)); } } private String getSWoTServiceNameFromUPnPServiceId(String id) { if (id.contains(UPnPUtil.SRV_RENDERING_CONTROL) || id.contains(UPnPUtil.SRV_AV_TRANSPORT)) { return "RenderingControl"; } else if (id.contains(UPnPUtil.SRV_CONTENT_DIRECTORY)) { return "ContentDirectory"; } return "unsupported"; } public interface UpdatePropertyInterface { void updateProperty(String name, Object value); } }