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