diff --git a/CODEOWNERS b/CODEOWNERS index 9379686ef47cf..566a6e8789f19 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -189,6 +189,7 @@ /bundles/org.openhab.binding.openwebnet/ @mvalla /bundles/org.openhab.binding.oppo/ @mlobstein /bundles/org.openhab.binding.orvibo/ @tavalin +/bundles/org.openhab.binding.panasonictv/ @J-N-K /bundles/org.openhab.binding.paradoxalarm/ @theater /bundles/org.openhab.binding.pentair/ @jsjames /bundles/org.openhab.binding.phc/ @gnlpfjh diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index ea69d3c0c1039..8a01a83fc6bac 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -936,6 +936,11 @@ org.openhab.binding.orvibo ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.panasonictv + ${project.version} + org.openhab.addons.bundles org.openhab.binding.paradoxalarm diff --git a/bundles/org.openhab.binding.panasonictv/NOTICE b/bundles/org.openhab.binding.panasonictv/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.panasonictv/README.md b/bundles/org.openhab.binding.panasonictv/README.md new file mode 100644 index 0000000000000..43f8dc83378bb --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/README.md @@ -0,0 +1,61 @@ +# Panasonic TV Binding + +This binding integrates the [Panasonic TV's](http://www.panasonic.com). + +## Supported Things + +Panasonic Viera TV (E6), models should be supported. +Panasonic TVs does not support full UPNP standard functionalities +Volume and Mute status are updated in real time +Most of the control is provided by sending Key Code to RemoteControl API + +Source Name (read only) is updated when a Key Code to change the source is sent to tv, otherwise it is Undefined + + +Tested TV models: + +| Model | State | Notes | +|-----------|---------|--------------------------------------------------------------------------------------| +| Viera E6 | OK | Initial contribution is done by this model | + + + +## Discovery + +The TV's are discovered through UPnP protocol in the local network and all devices are put in the Inbox. + +## Binding Configuration + +The binding does not require any special configuration. + +## Thing Configuration + +The Panasonic TV Thing requires the host name and port address as a configuration value in order for the binding to know how to access it. Panasonic TV publish several UPnP devices and hostname is used to recognize those UPnP devices. +Port address is used for remote control emulation protocol. +Additionally, a refresh interval can be configured in milliseconds to specify how often TV resources are polled. + +E.g. + +``` +Thing panasonictv:tv:livingroom [ hostName="192.168.1.10", port=55000, refreshInterval=1000 ] +``` + +## Channels + +TVs support the following channels: + +| Channel Type ID | Item Type | Description | +|------------------|-----------|---------------------------------------------------------------------------------------------------------| +| volume | Dimmer | Volume level of the TV. | +| mute | Switch | Mute state of the TV. | +| sourceName | String | Name of the current source. Readonly, updated blindly when keyCode to change input is sent | +| sourceId | Number | Id of the current source. | +| keyCode | String | The key code channel emulates the infrared remote controller and allows to send virtual button presses. | + +E.g. + +``` +Dimmer TV_Volume { channel="panasonictv:tv:livingroom:volume" } +Switch TV_Mute { channel="panasonictv:tv:livingroom:mute" } +String TV_KeyCode { channel="panasonictv:tv:livingroom:keyCode" } +``` diff --git a/bundles/org.openhab.binding.panasonictv/pom.xml b/bundles/org.openhab.binding.panasonictv/pom.xml new file mode 100644 index 0000000000000..428da13aaaa9f --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.panasonictv + + openHAB Add-ons :: Bundles :: Panasonic TV Binding + + diff --git a/bundles/org.openhab.binding.panasonictv/src/main/feature/feature.xml b/bundles/org.openhab.binding.panasonictv/src/main/feature/feature.xml new file mode 100644 index 0000000000000..f059397e4718b --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-upnp + mvn:org.openhab.addons.bundles/org.openhab.binding.panasonictv/${project.version} + + diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicTvBindingConstants.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicTvBindingConstants.java new file mode 100644 index 0000000000000..894eb5ff9faf3 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicTvBindingConstants.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link PanasonicTvBindingConstants} class defines common constants, which are used + * across the whole binding. + * + * @author Prakashbabu Sidaraddi - Initial contribution + */ +@NonNullByDefault +public class PanasonicTvBindingConstants { + + public static final String BINDING_ID = "panasonictv"; + + public static final ThingTypeUID THING_TYPE_PANASONICTV = new ThingTypeUID(BINDING_ID, "tv"); + + public static final String CONFIG_REMOTECONTROLLER_UDN = "remoteControllerUdn"; + public static final String CONFIG_MEDIARENDERER_UDN = "mediaRendererUdn"; + public static final String PROPERTY_SERIAL = "serialNumber"; + + // List of all remote controller thing channel id's + public static final String KEY_CODE = "keyCode"; + public static final String POWER = "power"; + public static final String SOURCE_NAME = "sourceName"; + public static final String SOURCE_ID = "sourceId"; + + // List of all media renderer thing channel id's + public static final String VOLUME = "volume"; + public static final String MUTE = "mute"; +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicTvHandlerFactory.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicTvHandlerFactory.java new file mode 100644 index 0000000000000..b0966cd701b6f --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicTvHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal; + +import static org.openhab.binding.panasonictv.internal.PanasonicTvBindingConstants.THING_TYPE_PANASONICTV; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.panasonictv.internal.handler.PanasonicTvHandler; +import org.openhab.core.io.transport.upnp.UpnpIOService; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link PanasonicTvHandlerFactory} is responsible for creating things and + * thing handlers. + * + * @author Prakashbabu Sidaraddi - Initial contribution + */ +@NonNullByDefault +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.panasonictv") +public class PanasonicTvHandlerFactory extends BaseThingHandlerFactory { + private static final Collection SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_PANASONICTV); + + private final UpnpIOService upnpIOService; + + @Activate + public PanasonicTvHandlerFactory(@Reference PanasonicUpnpIOService upnpIOService) { + this.upnpIOService = upnpIOService; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(THING_TYPE_PANASONICTV)) { + return new PanasonicTvHandler(thing, upnpIOService); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicUpnpIOService.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicUpnpIOService.java new file mode 100644 index 0000000000000..7ed16ee0d0ff1 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/PanasonicUpnpIOService.java @@ -0,0 +1,553 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +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.message.header.UDNHeader; +import org.jupnp.model.meta.Action; +import org.jupnp.model.meta.Device; +import org.jupnp.model.meta.DeviceIdentity; +import org.jupnp.model.meta.LocalDevice; +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.jupnp.registry.Registry; +import org.jupnp.registry.RegistryListener; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.io.transport.upnp.UpnpIOParticipant; +import org.openhab.core.io.transport.upnp.UpnpIOService; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PanasonicUpnpIOService} 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 + * @author Jan N. Klug - adapted findService to wrong namespace + */ +@SuppressWarnings({ "rawtypes" }) +@Component(immediate = true, service = PanasonicUpnpIOService.class) +public class PanasonicUpnpIOService implements UpnpIOService, RegistryListener { + + private final Logger logger = LoggerFactory.getLogger(PanasonicUpnpIOService.class); + + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(POOL_NAME); + + private static final int DEFAULT_POLLING_INTERVAL = 60; + private static final String POOL_NAME = "upnp-io"; + + private final UpnpService upnpService; + + final Set participants = new CopyOnWriteArraySet<>(); + final Map pollingJobs = new ConcurrentHashMap<>(); + final Map currentStates = new ConcurrentHashMap<>(); + final Map subscriptionCallbacks = new ConcurrentHashMap<>(); + + 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) { + if (Objects.equals(getDevice(participant), 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 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) { + if (Objects.equals(getDevice(participant), deviceRoot)) { + for (String stateVariable : values.keySet()) { + StateVariableValue value = values.get(stateVariable); + if (value != null && 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) { + if (Objects.equals(getDevice(participant), deviceRoot)) { + try { + participant.onServiceSubscribed(serviceId, false); + } catch (Exception e2) { + logger.error("Participant threw an exception onServiceSubscribed", e2); + } + } + } + } + } + + @Activate + public PanasonicUpnpIOService(final @Reference UpnpService upnpService) { + this.upnpService = upnpService; + } + + @Activate + public void activate() { + logger.debug("Starting UPnP IO service..."); + upnpService.getRegistry().getRemoteDevices().forEach(device -> informParticipants(device, true)); + upnpService.getRegistry().addListener(this); + } + + @Deactivate + public void deactivate() { + logger.debug("Stopping UPnP IO service..."); + upnpService.getRegistry().removeListener(this); + } + + private Device getDevice(UpnpIOParticipant participant) { + return upnpService.getRegistry().getDevice(new UDN(participant.getUDN()), true); + } + + @Override + public void addSubscription(UpnpIOParticipant participant, String serviceID, int duration) { + if (participant != null && serviceID != null) { + registerParticipant(participant); + Device device = getDevice(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 = getDevice(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()); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public Map invokeAction(UpnpIOParticipant participant, String serviceID, String actionID, + Map inputs) { + Map resultMap = new HashMap<>(); + + if (serviceID != null && actionID != null && participant != null) { + registerParticipant(participant); + Device device = getDevice(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 (inputs != null) { + for (String variable : inputs.keySet()) { + invocation.setInput(variable, inputs.get(variable)); + } + } + + logger.trace("Invoking Action '{}' of service '{}' for participant '{}'", 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 result = invocation.getOutputMap(); + if (result != null) { + for (String variable : result.keySet()) { + final ActionArgumentValue newArgument; + try { + newArgument = Objects.requireNonNull(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 '{}'", + 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) { + UDN udn = new UDN(participant.getUDN()); + if (upnpService.getRegistry().getDevice(udn, true) != null) { + return true; + } else { + upnpService.getControlPoint().search(new UDNHeader(udn)); + return false; + } + } + + @Override + public void registerParticipant(UpnpIOParticipant participant) { + if (participant != null) { + participants.add(participant); + } + } + + @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 = device.findService(new UDAServiceId(serviceID)); + if (service == null) { + String namespace = device.getType().getNamespace(); + service = device.findService(new ServiceId(namespace, serviceID)); + } + + return service; + } + + /** + * Propagates a device status change to all participants + * + * @param device the device that has changed its status + * @param status true, if device is reachable, false otherwise + */ + private void informParticipants(RemoteDevice device, boolean status) { + for (UpnpIOParticipant participant : participants) { + if (participant.getUDN().equals(device.getIdentity().getUdn().getIdentifierString())) { + setDeviceStatus(participant, status); + } + } + } + + private void setDeviceStatus(UpnpIOParticipant participant, boolean newStatus) { + if (!Objects.equals(currentStates.get(participant), newStatus)) { + currentStates.put(participant, newStatus); + logger.debug("Device '{}' reachability status changed to '{}'", participant.getUDN(), newStatus); + participant.onStatusChanged(newStatus); + } + } + + private class UPNPPollingRunnable implements Runnable { + + private final UpnpIOParticipant participant; + private final String serviceID; + private final 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 = getDevice(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); + logger.debug("Polling participant '{}' through Action '{}' of Service '{}' ", + participant.getUDN(), actionID, serviceID); + new ActionCallback.Default(invocation, upnpService.getControlPoint()).run(); + + // The UDN is reachable if no connection exception occurs + boolean status = true; + ActionException anException = invocation.getFailure(); + if (anException != null) { + String message = anException.getMessage(); + if (message != null && message.contains("Connection error or no response received")) { + // The UDN is not reachable anymore + status = false; + } + } + // Set status + setDeviceStatus(participant, status); + } 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.getMessage(), e); + } + } + } + + @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.scheduleWithFixedDelay(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); + } + } + + @Override + public void remoteDeviceAdded(Registry registry, RemoteDevice device) { + informParticipants(device, true); + } + + @Override + public void remoteDeviceUpdated(Registry registry, RemoteDevice device) { + } + + @Override + public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { + informParticipants(device, false); + } + + @Override + public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { + } + + @Override + public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) { + } + + @Override + public void localDeviceAdded(Registry registry, LocalDevice device) { + } + + @Override + public void localDeviceRemoved(Registry registry, LocalDevice device) { + } + + @Override + public void beforeShutdown(Registry registry) { + } + + @Override + public void afterShutdown() { + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/StatusEventDTO.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/StatusEventDTO.java new file mode 100644 index 0000000000000..61812d9bfebab --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/StatusEventDTO.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal; + +import java.util.*; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.adapters.XmlAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import org.w3c.dom.Element; + +/** + * The {@link StatusEventDTO} is responsible for + * + * @author Jan N. Klug - Initial contribution + */ +@XmlRootElement(name = "Event") +public class StatusEventDTO { + + @XmlElement(name = "InstanceID") + @XmlJavaTypeAdapter(InstanceIdAdapter.class) + public HashMap values; + + @Override + public String toString() { + return "StatusEventDTO{" + "values=" + values + '}'; + } + + public static class InstanceIdAdapter extends XmlAdapter> { + public static class ElementList { + @XmlAnyElement + public List elements = new ArrayList<>(); + } + + @Override + public ElementList marshal(Map map) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Map unmarshal(ElementList elementList) { + return elementList.elements.stream().collect( + Collectors.toMap(e -> e.getLocalName().toLowerCase(), e -> e.getAttributeNode("val").getValue())); + } + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/api/PanasonicEventListener.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/api/PanasonicEventListener.java new file mode 100644 index 0000000000000..f366e7e543a35 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/api/PanasonicEventListener.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.State; + +/** + * Interface for receiving data from Panasonic TV services. + * + * @author Pauli Anttila - Initial contribution + */ +@NonNullByDefault +public interface PanasonicEventListener { + /** + * Invoked when value is received from the TV. + * + * @param variable Name of the variable. + * @param value Value of the variable value. + */ + void valueReceived(String variable, State value); + + /** + * Report an error to this event listener + * + * @param statusDetail hint about the actual underlying problem + * @param message of the error + * @param e exception that might have occurred + */ + void reportError(ThingStatusDetail statusDetail, String message, @Nullable Throwable e); +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/api/PanasonicTvService.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/api/PanasonicTvService.java new file mode 100644 index 0000000000000..6ecd2e511b32e --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/api/PanasonicTvService.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.api; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.Command; + +/** + * Interface for Panasonic TV services. + * + * @author Pauli Anttila - Initial contribution + */ +@NonNullByDefault +public interface PanasonicTvService { + + /** + * Procedure to get list of supported channel names. + * + * @return List of supported + */ + Set getSupportedChannelNames(); + + /** + * Procedure for sending command. + * + * @param channel the channel to which the command applies + * @param command the command to be handled + */ + void handleCommand(String channel, Command command); + + /** + * Procedure for starting service. + * + */ + void start(); + + /** + * Procedure for stopping service. + * + */ + void stop(); + + /** + * Procedure for clearing internal caches. + * + */ + void clearCache(); + + /** + * get the service name of this service + * + * @return a String containing the service name + */ + String getServiceName(); +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/config/PanasonicTvConfiguration.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/config/PanasonicTvConfiguration.java new file mode 100644 index 0000000000000..9f92822272ce5 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/config/PanasonicTvConfiguration.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Configuration class for PanasonicTvHandler. + * + * @author Prakashbabu Sidaraddi - Initial contribution + */ +@NonNullByDefault +public class PanasonicTvConfiguration { + public String remoteControllerUdn = ""; + public String mediaRendererUdn = ""; + public int refreshInterval = 1000; + + @Override + public String toString() { + return "PanasonicTvConfiguration{" + "remoteControllerUdn='" + remoteControllerUdn + '\'' + + ", mediaRendererUdn='" + mediaRendererUdn + '\'' + ", refreshInterval=" + refreshInterval + '}'; + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/discovery/DeviceInformation.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/discovery/DeviceInformation.java new file mode 100644 index 0000000000000..9e108c62f0b41 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/discovery/DeviceInformation.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.discovery; + +import static org.openhab.binding.panasonictv.internal.PanasonicTvBindingConstants.THING_TYPE_PANASONICTV; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.model.meta.*; +import org.jupnp.model.types.DeviceType; +import org.jupnp.model.types.UDN; +import org.openhab.binding.panasonictv.internal.service.MediaRendererService; +import org.openhab.binding.panasonictv.internal.service.RemoteControllerService; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.util.UIDUtils; + +/** + * The {@link DeviceInformation} is a wrapper for device information + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class DeviceInformation { + public @Nullable ThingUID thingUid; + public String manufacturer; + public Map services = new HashMap<>(); + public String host; + public @Nullable String friendlyName; + public @Nullable String modelName; + public @Nullable String serialNumber; + + private DeviceInformation(@Nullable ThingUID thingUid, String manufacturer, String serviceType, String udn, + String host, @Nullable String friendlyName, @Nullable String modelName, @Nullable String serialNumber) { + this.thingUid = thingUid; + this.manufacturer = manufacturer; + this.services.put(serviceType, udn); + this.host = host; + this.friendlyName = friendlyName; + this.modelName = modelName; + this.serialNumber = serialNumber; + } + + public boolean isComplete() { + return services.containsKey(MediaRendererService.SERVICE_NAME) + && services.containsKey(RemoteControllerService.SERVICE_NAME); + } + + public DeviceInformation merge(@Nullable DeviceInformation deviceInformation) { + if (deviceInformation != null) { + this.services.putAll(deviceInformation.services); + if (deviceInformation.thingUid != null) { + thingUid = deviceInformation.thingUid; + } + } + return this; + } + + @Override + public String toString() { + return "DeviceInformation{" + "thingUid=" + thingUid + ", manufacturer='" + manufacturer + '\'' + ", services=" + + services + ", host='" + host + '\'' + ", friendlyName='" + friendlyName + '\'' + ", modelName='" + + modelName + '\'' + ", serialNumber='" + serialNumber + '\'' + '}'; + } + + /** + * get the device information from an UPnP RemoteDevice + * + * @param device a UPnP RemoteDevice + * @return Optional of the device information (empty if information not available) + */ + public static @Nullable DeviceInformation fromDevice(RemoteDevice device) { + DeviceDetails deviceDetails = device.getDetails(); + DeviceType deviceType = device.getType(); + RemoteDeviceIdentity deviceIdentity = device.getIdentity(); + if (deviceDetails == null || deviceType == null || deviceIdentity == null) { + return null; + } + + ManufacturerDetails manufacturerDetails = deviceDetails.getManufacturerDetails(); + UDN udn = deviceIdentity.getUdn(); + URL url = deviceIdentity.getDescriptorURL(); + if (udn == null || url == null) { + return null; + } + + ModelDetails modelDetails = deviceDetails.getModelDetails(); + String modelName = modelDetails == null ? null : modelDetails.getModelName(); + + // only generate the ThingUID for the media-renderer UDN + ThingUID thingUid = MediaRendererService.SERVICE_NAME.equals(deviceType.getType()) + ? new ThingUID(THING_TYPE_PANASONICTV, UIDUtils.encode(udn.getIdentifierString())) + : null; + + return new DeviceInformation(thingUid, manufacturerDetails.getManufacturer(), deviceType.getType(), + udn.getIdentifierString(), url.getHost(), deviceDetails.getFriendlyName(), modelName, + deviceDetails.getSerialNumber()); + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/discovery/PanasonicTvDiscoveryParticipant.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/discovery/PanasonicTvDiscoveryParticipant.java new file mode 100644 index 0000000000000..2df7dca47f4ca --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/discovery/PanasonicTvDiscoveryParticipant.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.discovery; + +import static org.openhab.binding.panasonictv.internal.PanasonicTvBindingConstants.*; + +import java.util.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jupnp.model.meta.*; +import org.openhab.binding.panasonictv.internal.service.MediaRendererService; +import org.openhab.binding.panasonictv.internal.service.RemoteControllerService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PanasonicTvDiscoveryParticipant} is responsible for processing the + * results of searched UPnP devices + * + * @author Prakashbabu Sidaraddi - Initial contribution + */ +@NonNullByDefault +@Component(service = UpnpDiscoveryParticipant.class) +public class PanasonicTvDiscoveryParticipant implements UpnpDiscoveryParticipant { + private final Logger logger = LoggerFactory.getLogger(PanasonicTvDiscoveryParticipant.class); + private final HashMap incompleteDiscoveryResults = new HashMap<>(); + + @Override + public Set getSupportedThingTypeUIDs() { + return Set.of(THING_TYPE_PANASONICTV); + } + + @Override + public @Nullable DiscoveryResult createResult(RemoteDevice device) { + DeviceInformation deviceInformation = DeviceInformation.fromDevice(device); + if (deviceInformation == null) { + return null; + } + + if (!deviceInformation.manufacturer.toUpperCase().contains("PANASONIC")) { + logger.trace("Ignoring {}: Not a Panasonic TV", deviceInformation); + return null; + } + + logger.debug("Processing {}", deviceInformation); + DeviceInformation resultingDeviceInformation = incompleteDiscoveryResults.compute(deviceInformation.host, + (k, v) -> deviceInformation.merge(v)); + if (resultingDeviceInformation == null || !resultingDeviceInformation.isComplete()) { + logger.debug("{} still incomplete", deviceInformation.host); + return null; + } + + ThingUID thingUid = resultingDeviceInformation.thingUid; + String mrUdn = resultingDeviceInformation.services.get(MediaRendererService.SERVICE_NAME); + String rcUdn = resultingDeviceInformation.services.get(RemoteControllerService.SERVICE_NAME); + + if (thingUid == null || mrUdn == null || rcUdn == null) { + logger.warn( + "Found a complete result but something is missing: thingUid={}, mrUdn={}, rcUdn={}. Please report a bug.", + thingUid, mrUdn, rcUdn); + return null; + } + + Map properties = new HashMap<>(); + properties.put(CONFIG_MEDIARENDERER_UDN, mrUdn); + properties.put(CONFIG_REMOTECONTROLLER_UDN, rcUdn); + String serialNumber = resultingDeviceInformation.serialNumber; + if (serialNumber != null) { + properties.put(PROPERTY_SERIAL, serialNumber); + } + + logger.debug("Created a DiscoveryResult for device '{}' ({}) with UDNs '{}' and '{}'", + resultingDeviceInformation.modelName, resultingDeviceInformation.host, mrUdn, rcUdn); + + String label = Objects.requireNonNullElse(resultingDeviceInformation.friendlyName, mrUdn); + return DiscoveryResultBuilder.create(thingUid).withProperties(properties) + .withRepresentationProperty(CONFIG_MEDIARENDERER_UDN).withLabel(label).build(); + } + + @Override + public @Nullable ThingUID getThingUID(RemoteDevice device) { + DeviceInformation deviceInformation = DeviceInformation.fromDevice(device); + return deviceInformation != null ? deviceInformation.thingUid : null; + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/handler/PanasonicTvHandler.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/handler/PanasonicTvHandler.java new file mode 100644 index 0000000000000..4ba3591d768d0 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/handler/PanasonicTvHandler.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.handler; + +import static org.openhab.binding.panasonictv.internal.PanasonicTvBindingConstants.POWER; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.panasonictv.internal.api.PanasonicEventListener; +import org.openhab.binding.panasonictv.internal.api.PanasonicTvService; +import org.openhab.binding.panasonictv.internal.config.PanasonicTvConfiguration; +import org.openhab.binding.panasonictv.internal.service.MediaRendererService; +import org.openhab.binding.panasonictv.internal.service.RemoteControllerService; +import org.openhab.core.io.transport.upnp.UpnpIOService; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PanasonicTvHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Prakashbabu Sidaraddi - Initial contribution + */ +@NonNullByDefault +public class PanasonicTvHandler extends BaseThingHandler implements PanasonicEventListener { + private Logger logger = LoggerFactory.getLogger(PanasonicTvHandler.class); + + /* Global configuration for Panasonic TV Thing */ + private PanasonicTvConfiguration configuration = new PanasonicTvConfiguration(); + + private final UpnpIOService upnpIOService; + + /* Panasonic TV services */ + private final List services = new CopyOnWriteArrayList<>(); + private boolean powerState = false; + + public PanasonicTvHandler(Thing thing, UpnpIOService upnpIOService) { + super(thing); + + this.upnpIOService = upnpIOService; + logger.debug("Create a Panasonic TV Handler for thing '{}'", getThing().getUID()); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Received channel: {}, command: {}", channelUID, command); + + String channel = channelUID.getId(); + // Delegate command to correct service + services.stream().filter(service -> service.getSupportedChannelNames().contains(channel)).findAny() + .ifPresent(service -> service.handleCommand(channel, command)); + } + + @Override + public void channelLinked(ChannelUID channelUID) { + logger.debug("channelLinked: {}", channelUID); + + updateState(POWER, OnOffType.from(powerState)); + services.forEach(PanasonicTvService::clearCache); + } + + @Override + public void initialize() { + configuration = getConfigAs(PanasonicTvConfiguration.class); + + logger.debug("Initializing Panasonic TV handler for uid '{}' with configuration `{}`", getThing().getUID(), + configuration); + if (configuration.mediaRendererUdn.isEmpty() || configuration.remoteControllerUdn.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "UDNs must not be empty."); + return; + } + + updateStatus(ThingStatus.UNKNOWN); + + try { + services.add(new MediaRendererService(scheduler, upnpIOService, configuration.mediaRendererUdn, + configuration.refreshInterval, this)); + services.add(new RemoteControllerService(scheduler, upnpIOService, configuration.remoteControllerUdn, + configuration.refreshInterval, this)); + services.forEach(PanasonicTvService::start); + } catch (IllegalStateException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "Could not initialize services."); + services.forEach(PanasonicTvService::stop); + } + } + + @Override + public void dispose() { + shutdown(); + services.clear(); + } + + private void shutdown() { + logger.debug("Shutdown all Panasonic TV services"); + services.forEach(PanasonicTvService::stop); + } + + private void setThingAndPowerState(boolean powerState) { + if (this.powerState != powerState) { + this.powerState = powerState; + updateState(POWER, OnOffType.from(powerState)); + updateStatus(powerState ? ThingStatus.ONLINE : ThingStatus.OFFLINE); + } + } + + @Override + public void valueReceived(String variable, State value) { + logger.debug("Received value '{}':'{}' for thing '{}'", variable, value, this.getThing().getUID()); + updateState(variable, value); + setThingAndPowerState(true); + } + + @Override + public void reportError(ThingStatusDetail statusDetail, String message, @Nullable Throwable e) { + logger.debug("Error was reported: {}", message, e); + updateStatus(ThingStatus.OFFLINE, statusDetail, message); + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/AbstractPanasonicTvService.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/AbstractPanasonicTvService.java new file mode 100644 index 0000000000000..6ff9b941fe8c1 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/AbstractPanasonicTvService.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.service; + +import java.io.StringReader; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import javax.xml.bind.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.panasonictv.internal.StatusEventDTO; +import org.openhab.binding.panasonictv.internal.api.PanasonicEventListener; +import org.openhab.binding.panasonictv.internal.api.PanasonicTvService; +import org.openhab.core.io.transport.upnp.UpnpIOParticipant; +import org.openhab.core.io.transport.upnp.UpnpIOService; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AbstractPanasonicTvService} is responsible for + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractPanasonicTvService implements UpnpIOParticipant, PanasonicTvService { + private final Logger logger = LoggerFactory.getLogger(AbstractPanasonicTvService.class); + + protected final PanasonicEventListener listener; + protected final String udn; + private final String serviceId; + private final String serviceName; + private final Map> converters; + private final Set supportedCommands; + private final ScheduledExecutorService scheduler; + private final int refreshInterval; + + private final Unmarshaller eventUnmarshaller; + + protected final UpnpIOService service; + + protected final Map stateMap = new ConcurrentHashMap<>(); + + private @Nullable ScheduledFuture pollingJob; + + protected boolean eventsSubscribed = false; + + public AbstractPanasonicTvService(String udn, UpnpIOService service, PanasonicEventListener listener, + ScheduledExecutorService scheduler, int refreshInterval, String serviceName, String serviceId, + Set supportedCommands, Map> converters) { + this.udn = udn; + this.service = service; + this.listener = listener; + this.scheduler = scheduler; + this.refreshInterval = refreshInterval; + this.serviceId = serviceId; + this.serviceName = serviceName; + this.converters = converters; + this.supportedCommands = supportedCommands; + + try { + JAXBContext jaxbContext = JAXBContext.newInstance(StatusEventDTO.class); + eventUnmarshaller = jaxbContext.createUnmarshaller(); + } catch (JAXBException e) { + logger.warn("Could not create unmarshaller for events: {}", e.getMessage()); + throw new IllegalStateException(); + } + } + + @Override + public void onServiceSubscribed(@Nullable String service, boolean succeeded) { + logger.trace("Subscribed to service {} : {}", service, succeeded); + if (serviceId.equals(service)) { + eventsSubscribed = succeeded; + } + } + + @Override + public void onStatusChanged(boolean status) { + logger.debug("{} changed status: {}", serviceName, status); + } + + @Override + public String getUDN() { + return udn; + } + + @Override + public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) { + logger.trace("Received: service='{}', variable='{}', value='{}'", service, variable, value); + if ("LastChange".equals(variable) && value != null) { + try { + StatusEventDTO statusEvent = (StatusEventDTO) eventUnmarshaller.unmarshal(new StringReader(value)); + logger.debug("Extracted: {}", statusEvent); + } catch (JAXBException e) { + // TODO: ignore for now while we are still testing + } + } + + List converters = this.converters.get(variable); + + if (variable == null || value == null || converters == null) { + return; + } + + // put returns previous value or null if no previous value + if (value.equals(stateMap.put(variable, value))) { + logger.trace("Value '{}' for {} hasn't changed, ignoring update", value, variable); + return; + } + + converters.forEach(converter -> listener.valueReceived(converter.channelName, converter.function.apply(value))); + } + + @Override + public String getServiceName() { + return serviceName; + } + + @Override + public Set getSupportedChannelNames() { + return supportedCommands; + } + + @Override + public abstract void handleCommand(String channel, Command command); + + @Override + public void clearCache() { + stateMap.clear(); + } + + static class ChannelConverter { + public String channelName; + public Function function; + + public ChannelConverter(String channelName, Function function) { + this.channelName = channelName; + this.function = function; + } + } + + @Override + public void start() { + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + stop(); + } + logger.debug("Start refresh task, interval={}", refreshInterval); + this.pollingJob = scheduler.scheduleWithFixedDelay(this::internalPolling, 0, refreshInterval, + TimeUnit.MILLISECONDS); + } + + @Override + public void stop() { + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + this.pollingJob = null; + } + service.removeSubscription(this, serviceId); + } + + protected boolean isRegistered() { + return service.isRegistered(this); + } + + protected void reportError(String message, @Nullable Throwable e) { + listener.reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e); + } + + private void internalPolling() { + logger.trace("polling {}", serviceName); + if (isRegistered()) { + if (!eventsSubscribed) { + service.addSubscription(this, serviceId, 600); + } + polling(); + } else { + logger.debug("Service not registered, skipping polling, trying to register."); + reportError("UPnP device registration not found", null); + service.registerParticipant(this); + } + } + + protected abstract void polling(); + + protected void updateResourceState(String serviceId, String actionId, Map inputs) { + service.invokeAction(this, serviceId, actionId, inputs).forEach((k, v) -> onValueReceived(k, v, serviceId)); + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/DataConverters.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/DataConverters.java new file mode 100644 index 0000000000000..1632e72ae23e6 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/DataConverters.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.service; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.types.Command; + +/** + * The {@link DataConverters} provides utils for converting openHAB commands to + * Panasonic TV specific values. + * + * @author Pauli Anttila - Initial contribution + */ +@NonNullByDefault +public class DataConverters { + + /** + * Convert openHAB command to int. + * + * @param command + * @param min + * @param max + * @param currentValue + * @return + */ + public static int convertCommandToIntValue(Command command, int min, int max, int currentValue) { + if (command instanceof IncreaseDecreaseType || command instanceof DecimalType) { + int value; + if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) { + value = Math.min(max, currentValue + 1); + } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) { + value = Math.max(min, currentValue - 1); + } else if (command instanceof DecimalType) { + value = ((DecimalType) command).intValue(); + } else { + throw new NumberFormatException("Command '" + command + "' not supported"); + } + + return value; + } else { + throw new NumberFormatException("Command '" + command + "' not supported"); + } + } + + /** + * Convert openHAB command to boolean. + * + * @param command + * @return + */ + public static boolean convertCommandToBooleanValue(Command command) { + if (command instanceof OnOffType || command instanceof OpenClosedType || command instanceof UpDownType) { + boolean newValue; + + if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP) || command.equals(OpenClosedType.OPEN)) { + newValue = true; + } else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN) + || command.equals(OpenClosedType.CLOSED)) { + newValue = false; + } else { + throw new NumberFormatException("Command '" + command + "' not supported"); + } + + return newValue; + } else { + throw new NumberFormatException("Command '" + command + "' not supported for channel"); + } + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/MediaRendererService.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/MediaRendererService.java new file mode 100644 index 0000000000000..aa51a33f54d1e --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/MediaRendererService.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.service; + +import static org.openhab.binding.panasonictv.internal.PanasonicTvBindingConstants.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.panasonictv.internal.api.PanasonicEventListener; +import org.openhab.core.io.transport.upnp.UpnpIOService; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link MediaRendererService} is responsible for handling MediaRenderer + * commands. + * + * @author Prakashbabu Sidaraddi - Initial contribution + */ +@NonNullByDefault +public class MediaRendererService extends AbstractPanasonicTvService { + public static final String SERVICE_NAME = "MediaRenderer"; + private static final String SERVICE_ID = "RenderingControl"; + private static final Set SUPPORTED_COMMANDS = Set.of(VOLUME, MUTE); + private static final Map> CONVERTERS = Map.of("CurrentVolume", + List.of(new ChannelConverter(VOLUME, PercentType::new)), "CurrentMute", + List.of(new ChannelConverter(MUTE, v -> OnOffType.from(v.equals("true"))))); + + private final Logger logger = LoggerFactory.getLogger(MediaRendererService.class); + + public MediaRendererService(ScheduledExecutorService scheduler, UpnpIOService upnpIOService, String udn, + int refreshInterval, PanasonicEventListener eventListener) { + super(udn, upnpIOService, eventListener, scheduler, refreshInterval, SERVICE_NAME, SERVICE_ID, + SUPPORTED_COMMANDS, CONVERTERS); + logger.debug("Create a Panasonic TV MediaRenderer service"); + } + + @Override + protected void polling() { + try { + updateResourceState(SERVICE_ID, "GetVolume", Map.of("InstanceID", "0", "Channel", "Master")); + updateResourceState(SERVICE_ID, "GetMute", Map.of("InstanceID", "0", "Channel", "Master")); + } catch (Exception e) { + reportError("Error occurred during poll", e); + } + } + + @Override + public void handleCommand(String channel, Command command) { + logger.debug("Received channel: {}, command: {}", channel, command); + + switch (channel) { + case VOLUME: + setVolume(command); + break; + case MUTE: + setMute(command); + break; + default: + logger.warn("Panasonic TV doesn't support transmitting for channel '{}'", channel); + } + } + + private void setVolume(Command command) { + int newValue; + + try { + newValue = DataConverters.convertCommandToIntValue(command, 0, 100, + Integer.parseInt(stateMap.getOrDefault("CurrentVolume", ""))); + } catch (NumberFormatException e) { + throw new NumberFormatException("Command '" + command + "' not supported"); + } + + updateResourceState(SERVICE_ID, "SetVolume", + Map.of("InstanceID", "0", "Channel", "Master", "DesiredVolume", Integer.toString(newValue))); + updateResourceState(SERVICE_ID, "GetVolume", Map.of("InstanceID", "0", "Channel", "Master")); + } + + private void setMute(Command command) { + boolean newValue; + + try { + newValue = DataConverters.convertCommandToBooleanValue(command); + } catch (NumberFormatException e) { + throw new NumberFormatException("Command '" + command + "' not supported"); + } + + updateResourceState(SERVICE_ID, "SetMute", + Map.of("InstanceID", "0", "Channel", "Master", "DesiredMute", Boolean.toString(newValue))); + updateResourceState(SERVICE_ID, "GetMute", Map.of("InstanceID", "0", "Channel", "Master")); + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/RemoteControllerService.java b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/RemoteControllerService.java new file mode 100644 index 0000000000000..792e2af910179 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/java/org/openhab/binding/panasonictv/internal/service/RemoteControllerService.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv.internal.service; + +import static org.openhab.binding.panasonictv.internal.PanasonicTvBindingConstants.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.panasonictv.internal.api.PanasonicEventListener; +import org.openhab.core.io.transport.upnp.UpnpIOService; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RemoteControllerService} is responsible for handling remote + * controller commands. + * + * @author Prakashbabu Sidaraddi - Initial contribution + */ +@NonNullByDefault +public class RemoteControllerService extends AbstractPanasonicTvService { + public static final String SERVICE_NAME = "p00RemoteController"; + private static final String SERVICE_ID = "p00NetworkControl"; + private static final Set SUPPORTED_COMMANDS = Set.of(KEY_CODE); + private static final Map> CONVERTERS = Map.of("sourceName", List + .of(new ChannelConverter(SOURCE_NAME, StringType::new), new ChannelConverter(SOURCE_ID, StringType::new))); + private static final Set TV_INPUT_KEY_CODES = Set.of("NRC_HDMI1-ONOFF", "NRC_HDMI2-ONOFF", + "NRC_HDMI3-ONOFF", "NRC_HDMI4-ONOFF", "NRC_TV-ONOFF", "NRC_VIDEO1-ONOFF", "NRC_VIDEO2-ONOFF"); + + private final Logger logger = LoggerFactory.getLogger(RemoteControllerService.class); + + public RemoteControllerService(ScheduledExecutorService scheduler, UpnpIOService service, String udn, + int refreshInterval, PanasonicEventListener listener) { + super(udn, service, listener, scheduler, refreshInterval, SERVICE_NAME, SERVICE_ID, SUPPORTED_COMMANDS, + CONVERTERS); + + logger.debug("Create a Panasonic TV RemoteController service"); + } + + @Override + public void handleCommand(String channel, Command command) { + logger.debug("Received channel: {}, command: {}", channel, command); + + switch (channel) { + case KEY_CODE: + if (command instanceof StringType) { + sendKeyCode(command.toString().toUpperCase()); + } else { + logger.warn("Command '{}' not supported for channel '{}'", command, channel); + } + break; + } + } + + /** + * Sends a key code to Panasonic TV device. + * + * @param key Button code to send + */ + private void sendKeyCode(final String key) { + updateResourceState(SERVICE_ID, "X_SendKey", Map.of("X_KeyEvent", key)); + + if (TV_INPUT_KEY_CODES.contains(key)) { + onValueReceived("sourceName", key.substring(4, key.length() - 6), "p00NetworkControl"); + } + } + + @Override + protected void polling() { + // nothing to do here + } +} diff --git a/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..3e221d4155c2c --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Panasonic TV Binding + This is the binding for Panasonic TV. Binding should support all Panasonic Viera TV models + Prakashbabu Sidaraddi + + diff --git a/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..37ee41e4939b3 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,24 @@ + + + + + + + The UDN of this device's remote controller service + + + + The UDN of this device's media renderer service + + + + States how often a refresh shall occur in milliseconds. + + 1000 + + + + diff --git a/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/i18n/panasonic_de.properties b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/i18n/panasonic_de.properties new file mode 100644 index 0000000000000..0b700f7a753bd --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/i18n/panasonic_de.properties @@ -0,0 +1,27 @@ +# binding +binding.panasonictv.name = Panasonic TV Binding +binding.panasonictv.description = Dieses Binding integriert Panasonic TV Geräte, wodurch diese gesteuert werden können. + +# thing types +thing-type.panasonictv.tv.label = Panasonic TV +thing-type.panasonictv.tv.description = Dient zur Steuerung des Gerätes und liefert Daten wie z.B. Infos über den aktuellen Kanal oder die laufende Sendung. + +# thing types config +thing-type.config.panasonictv.tv.hostName.label = IP-Adresse +thing-type.config.panasonictv.tv.hostName.description = Lokale IP-Adresse oder Hostname des Panasonic TV. +thing-type.config.panasonictv.tv.port.label = Port +thing-type.config.panasonictv.tv.port.description = Port des Panasonic TV. +thing-type.config.panasonictv.tv.refreshInterval.label = Abfrageintervall +thing-type.config.panasonictv.tv.refreshInterval.description = Intervall zur Abfrage des Panasonic TV (in Millisekunden). + +# channel types +channel-type.panasonictv.volume.label = Lautstärke +channel-type.panasonictv.volume.description = Ermöglicht die Steuerung der Lautstärke. +channel-type.panasonictv.mute.label = Stumm schalten +channel-type.panasonictv.mute.description = Ermöglicht die Lautstärke auf stumm zu schalten. +channel-type.panasonictv.sourcename.label = Source +channel-type.panasonictv.sourcename.description = Zeigt die Quelle des eingehenden Signals an. +channel-type.panasonictv.sourceid.label = Source ID +channel-type.panasonictv.sourceid.description = Zeigt die ID der Quelle des eingehenden Signals an. +channel-type.panasonictv.keycode.label = Tastendruck +channel-type.panasonictv.keycode.description = Ermöglicht das Senden eines Eingabebefehls an das Gerät. diff --git a/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 0000000000000..6eb0eb89ed1d7 --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,129 @@ + + + + + Dimmer + + Volume level of the TV. + SoundVolume + + + + Switch + + Mute state of the TV. + + + + String + + Name of the current source. + + + + + String + + Id of the current source. + + + + + String + + The key code channel emulates the infrared remote controller and + allows to send virtual button presses. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..5aafd6b8a4bfa --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,23 @@ + + + + + + Allows to control Panasonic TV + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.panasonictv/src/test/java/org/openhab/binding/panasonictv/StatusEventTests.java b/bundles/org.openhab.binding.panasonictv/src/test/java/org/openhab/binding/panasonictv/StatusEventTests.java new file mode 100644 index 0000000000000..5bd6bce9c915d --- /dev/null +++ b/bundles/org.openhab.binding.panasonictv/src/test/java/org/openhab/binding/panasonictv/StatusEventTests.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.panasonictv; + +import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.panasonictv.internal.PanasonicTvBindingConstants.MUTE; +import static org.openhab.binding.panasonictv.internal.PanasonicTvBindingConstants.VOLUME; + +import java.io.StringReader; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.panasonictv.internal.StatusEventDTO; + +/** + * The {@link StatusEventTests} is a test class for status events + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class StatusEventTests { + + @Test + public void parseTest() throws JAXBException { + // @formatter:off + String xmlMessage = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + // @formatter:on + + JAXBContext jc = JAXBContext.newInstance(StatusEventDTO.class); + Unmarshaller un = jc.createUnmarshaller(); + xmlMessage = xmlMessage.replaceAll("xmlns(.*?)>", ">"); + StatusEventDTO statusEvent = (StatusEventDTO) un.unmarshal(new StringReader(xmlMessage)); + + assertNotNull(statusEvent); + assertNotNull(statusEvent.values); + + assertEquals(true, statusEvent.values.containsKey(VOLUME)); + assertEquals(true, statusEvent.values.containsKey(MUTE)); + assertEquals(11, statusEvent.values.size()); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 11b1484ea50d0..0f65386377245 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -221,6 +221,7 @@ org.openhab.binding.openwebnet org.openhab.binding.oppo org.openhab.binding.orvibo + org.openhab.binding.panasonictv org.openhab.binding.paradoxalarm org.openhab.binding.pentair org.openhab.binding.phc