diff --git a/CODEOWNERS b/CODEOWNERS
index 4f4cbc4e7a4a4..39a6730d6f0f8 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -197,6 +197,7 @@
/bundles/org.openhab.binding.paradoxalarm/ @theater
/bundles/org.openhab.binding.pentair/ @jsjames
/bundles/org.openhab.binding.phc/ @gnlpfjh
+/bundles/org.openhab.binding.pilight/ @stefanroellin @niklasdoerfler
/bundles/org.openhab.binding.pioneeravr/ @Stratehm
/bundles/org.openhab.binding.pixometer/ @Confectrician
/bundles/org.openhab.binding.pjlinkdevice/ @nils
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 33a19ae51f577..fa5dee36364ba 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -971,6 +971,11 @@
org.openhab.binding.phc
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.pilight
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.pioneeravr
diff --git a/bundles/org.openhab.binding.pilight/NOTICE b/bundles/org.openhab.binding.pilight/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/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.pilight/README.md b/bundles/org.openhab.binding.pilight/README.md
new file mode 100644
index 0000000000000..3a67884855963
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/README.md
@@ -0,0 +1,107 @@
+# pilight Binding
+
+The pilight binding allows openHAB to communicate with a [pilight](http://www.pilight.org/) instance running pilight version 6.0 or greater.
+
+> pilight is a free open source full fledge domotica solution that runs on a Raspberry Pi, HummingBoard, BananaPi, Radxa, but also on *BSD and various linuxes (tested on Arch, Ubuntu and Debian). It's open source and freely available for anyone. pilight works with a great deal of devices and is frequency independent. Therefor, it can control devices working at 315Mhz, 433Mhz, 868Mhz etc. Support for these devices are dependent on community, because we as developers don't own them all.
+
+pilight is a cheap way to control 'Click On Click Off' devices. It started as an application for the Raspberry Pi (using the GPIO interface) but it's also possible now to connect it to any other PC using an Arduino Nano. You will need a cheap 433Mhz transceiver in both cases. See the [Pilight manual](https://manual.pilight.org/electronics/wiring.html) for more information.
+
+## Supported Things
+
+| Thing | Type | Description |
+|-----------|--------|----------------------------------------------------------------------------|
+| `bridge` | Bridge | Pilight bridge required for the communication with the pilight daemon. |
+| `contact` | Thing | Pilight contact (read-only). |
+| `dimmer` | Thing | Pilight dimmer. |
+| `switch` | Thing | Pilight switch. |
+| `generic` | Thing | Pilight generic device for which you have to add the channels dynamically. |
+
+## Binding Configuration
+
+Things can be configured using Paper UI, or using a `.things` file.
+The configuration in this documentation explains the `.things` file, although you can find the same parameters from the Paper UI.
+
+### `bridge` Thing
+
+A `bridge` is required for the communication with a pilight daemon. Multiple pilight instances are supported by creating different pilight `bridge` things.
+
+The `bridge` requires the following configuration parameters:
+
+| Parameter Label | Parameter ID | Description | Required |
+|-----------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
+| IP Address | ipAddress | Host name or IP address of the pilight daemon | yes |
+| Port | port | Port number on which the pilight daemon is listening. Default: 5000 | yes |
+| Delay | delay | Delay (in millisecond) between consecutive commands. Recommended value without band pass filter: 1000. Recommended value with band pass filter: somewhere between 200-500. Default: 500 | no |
+
+Important: you must explicitly configure the port in the pilight daemon config or otherwise a random port will be used and the binding will not be able to connect.
+
+
+### `contact`, `dimmer`, `switch`, `generic` Things
+
+These things have alle one required parameter:
+
+| Parameter Label | Parameter ID | Description | Required |
+|-----------------|--------------|------------------------|----------|
+| Name | name | Name of pilight device | yes |
+
+
+## Channels
+
+The `bridge` thing has no channels.
+
+The `contact`, `dimmer` and `switch` things all have one channel:
+
+| Thing | Channel | Type | Description |
+|-----------|----------|---------|-------------------------|
+| `contact` | state | Contact | State of the contact |
+| `dimmer` | dimlevel | Dimmer | Dim level of the dimmer |
+| `switch` | state | Switch | State of the switch |
+
+The `generic` thing has no fixed channels and you have to add them manually. Currently, only String and Number channels are supported.
+
+## Auto Discovery
+
+### Bridge Auto Discovery
+
+The pilight daemon implements a SSDP interface, which can be used to search for running pilight daemon instances by sending a SSDP request via multicast udp (this mechanism may only work if the [standalone mode](https://manual.pilight.org/configuration/settings.html#standalone) in the pilight daemon is disabled.
+After loading the binding this bridge discovery is automatically run and scheduled to scan for bridges every 10 minutes.
+
+### Device Auto Discovery
+After a `bridge` thing has been configured in openHAB, it automatically establishes a connection between pilight daemon and openHAB.
+As soon as the bridge is connected, the devices configured in the pilight daemon are automatically found via autodiscovery in background (or via a manually triggered discovery) and are displayed in the inbox to easily create things from them.
+
+## Examples
+
+things/pilight.things
+
+```
+Bridge pilight:bridge:raspi "Pilight Daemon raspi" [ ipAddress="192.168.1.1", port=5000 ] {
+ Thing switch office "Office" [ name="office" ]
+ Thing dimmer piano "Piano" [ name="piano" ]
+ Thing generic weather "Weather" [ name="weather" ] {
+ Channels:
+ State Number : temperature [ property="temperature"]
+ State Number : humidity [ property="humidity"]
+ }
+}
+```
+
+items/pilight.items
+
+```
+Switch office_switch "Büro" { channel="pilight:switch:raspi:office:state" }
+Dimmer piano_light "Klavier [%.0f %%]" { channel="pilight:dimmer:raspi:piano:dimlevel" }
+Number weather_temperature "Aussentemperatur [%.1f °C]" { channel="pilight:generic:raspi:weather:temperature" }
+Number weather_humidity "Feuchtigkeit [%.0f %%]" { channel="pilight:generic:raspi:weather:humidity" }
+
+```
+
+sitemaps/fragment.sitemap
+
+```
+Switch item=office_switch
+Slider item=piano_light
+Text item=weather_temperature
+Text item=weather_humidity
+```
+
diff --git a/bundles/org.openhab.binding.pilight/pom.xml b/bundles/org.openhab.binding.pilight/pom.xml
new file mode 100644
index 0000000000000..54e96f45dc702
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.pilight
+
+ openHAB Add-ons :: Bundles :: Pilight Binding
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.10.4
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.10.4
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.10.4
+
+
+
+
diff --git a/bundles/org.openhab.binding.pilight/src/main/feature/feature.xml b/bundles/org.openhab.binding.pilight/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..e78e55f8707fb
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/feature/feature.xml
@@ -0,0 +1,23 @@
+
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.pilight/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/IPilightCallback.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/IPilightCallback.java
new file mode 100644
index 0000000000000..3d3fd8aa18102
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/IPilightCallback.java
@@ -0,0 +1,64 @@
+/**
+ * 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.pilight.internal;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.dto.Config;
+import org.openhab.binding.pilight.internal.dto.Status;
+import org.openhab.binding.pilight.internal.dto.Version;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+
+/**
+ * Callback interface to signal any listeners that an update was received from pilight
+ *
+ * @author Jeroen Idserda - Initial contribution
+ * @author Stefan Röllin - Port to openHAB 2 pilight binding
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public interface IPilightCallback {
+
+ /**
+ * Update thing status
+ *
+ * @param status status of thing
+ * @param statusDetail status detail of thing
+ * @param description description of thing status
+ */
+ void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description);
+
+ /**
+ * Update for one or more device received.
+ *
+ * @param allStatus list of Object containing list of devices that were updated and their current state
+ */
+ void statusReceived(List allStatus);
+
+ /**
+ * Configuration received.
+ *
+ * @param config Object containing configuration of pilight
+ */
+ void configReceived(Config config);
+
+ /**
+ * Version information received.
+ *
+ * @param version Object containing software version information of pilight daemon
+ */
+ void versionReceived(Version version);
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PiLightConnector.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PiLightConnector.java
new file mode 100644
index 0000000000000..b126b29e0d309
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PiLightConnector.java
@@ -0,0 +1,281 @@
+/**
+ * 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.pilight.internal;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.net.Socket;
+import java.util.Collections;
+import java.util.Date;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.dto.*;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.MappingJsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * This class listens for updates from the pilight daemon. It is also responsible for requesting
+ * and propagating the current pilight configuration.
+ *
+ * @author Jeroen Idserda - Initial contribution
+ * @author Stefan Röllin - Port to openHAB 2 pilight binding
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ *
+ */
+@NonNullByDefault
+public class PilightConnector extends Thread {
+
+ private static final Integer RECONNECT_DELAY_MSEC = 10 * 1000; // 10 seconds
+
+ private final Logger logger = LoggerFactory.getLogger(PilightConnector.class);
+
+ private final PilightBridgeConfiguration config;
+
+ private final IPilightCallback callback;
+
+ private final ObjectMapper inputMapper = new ObjectMapper(
+ new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false));
+
+ private final ObjectMapper outputMapper = new ObjectMapper(
+ new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false))
+ .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
+
+ private boolean running = true;
+
+ private @Nullable Socket socket;
+ private @Nullable PrintStream printStream;
+
+ private Date lastUpdate = new Date(0);
+
+ private ExecutorService delayedUpdateThreadPool = Executors.newSingleThreadExecutor();
+
+ public PilightConnector(PilightBridgeConfiguration config, IPilightCallback callback) {
+ this.config = config;
+ this.callback = callback;
+ setDaemon(true);
+ }
+
+ @Override
+ public void run() {
+ connect();
+
+ while (running) {
+ try {
+ final @Nullable Socket socket = this.socket;
+ if (socket != null && !socket.isClosed()) {
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ String line = in.readLine();
+ while (running && line != null) {
+ if (!line.isEmpty()) {
+ logger.trace("Received from pilight: {}", line);
+ if (line.startsWith("{\"message\":\"config\"")) {
+ // Configuration received
+ logger.debug("Config received");
+ callback.configReceived(inputMapper.readValue(line, Message.class).getConfig());
+ } else if (line.startsWith("{\"message\":\"values\"")) {
+ AllStatus status = inputMapper.readValue(line, AllStatus.class);
+ callback.statusReceived(status.getValues());
+ } else if (line.startsWith("{\"version\":")) {
+ Version version = inputMapper.readValue(line, Version.class);
+ callback.versionReceived(version);
+ logger.debug("version received: {}", line);
+ } else if (line.startsWith("{\"status\":")) {
+ // Status message, we're not using this for now.
+ Response response = inputMapper.readValue(line, Response.class);
+ logger.trace("Response success: {}", response.isSuccess());
+ } else if (line.equals("1")) {
+ // pilight stopping
+ throw new IOException("Connection to pilight lost");
+ } else {
+ Status status = inputMapper.readValue(line, Status.class);
+ callback.statusReceived(Collections.singletonList(status));
+ }
+ }
+ line = in.readLine();
+ }
+ }
+ } catch (IOException e) {
+ if (running) {
+ logger.debug("Error in pilight listener thread", e);
+ }
+ }
+
+ logger.info("Disconnected from pilight server at {}:{}", config.getIpAddress(), config.getPort());
+
+ if (running) {
+ callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, null);
+ // empty line received (socket closed) or pilight stopped but binding
+ // is still running, try to reconnect
+ connect();
+ }
+ }
+ }
+
+ /**
+ * Tells the connector to refresh the configuration
+ */
+ public void refreshConfig() {
+ logger.trace("refreshConfig");
+ doSendAction(new Action(Action.ACTION_REQUEST_CONFIG));
+ }
+
+ /**
+ * Tells the connector to refresh the status of all devices
+ */
+ public void refreshStatus() {
+ logger.trace("refreshStatus");
+ doSendAction(new Action(Action.ACTION_REQUEST_VALUES));
+ }
+
+ /**
+ * Stops the listener
+ */
+ public void close() {
+ running = false;
+ disconnect();
+ interrupt();
+ }
+
+ private void disconnect() {
+ final @Nullable PrintStream printStream = this.printStream;
+ if (printStream != null) {
+ printStream.close();
+ this.printStream = null;
+ }
+
+ final @Nullable Socket socket = this.socket;
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ logger.debug("Error while closing pilight socket", e);
+ }
+ this.socket = null;
+ }
+ }
+
+ private boolean isConnected() {
+ final @Nullable Socket socket = this.socket;
+ return socket != null && !socket.isClosed();
+ }
+
+ private void connect() {
+ disconnect();
+
+ int delay = 0;
+
+ while (!isConnected()) {
+ try {
+ logger.debug("pilight connecting to {}:{}", config.getIpAddress(), config.getPort());
+
+ Thread.sleep(delay);
+ Socket socket = new Socket(config.getIpAddress(), config.getPort());
+
+ Options options = new Options();
+ options.setConfig(true);
+
+ Identification identification = new Identification();
+ identification.setOptions(options);
+
+ // For some reason, directly using the outputMapper to write to the socket's OutputStream doesn't work.
+ PrintStream printStream = new PrintStream(socket.getOutputStream(), true);
+ printStream.println(outputMapper.writeValueAsString(identification));
+
+ Response response = inputMapper.readValue(socket.getInputStream(), Response.class);
+
+ if (response.getStatus().equals(Response.SUCCESS)) {
+ logger.info("Established connection to pilight server at {}:{}", config.getIpAddress(),
+ config.getPort());
+ this.socket = socket;
+ this.printStream = printStream;
+ callback.updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
+ } else {
+ socket.close();
+ logger.debug("pilight client not accepted: {}", response.getStatus());
+ }
+ } catch (IOException e) {
+ logger.debug("connect failed", e);
+ callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ } catch (InterruptedException e) {
+ logger.debug("connect interrupted", e);
+ }
+
+ delay = RECONNECT_DELAY_MSEC;
+ }
+ }
+
+ /**
+ * send action to pilight daemon
+ *
+ * @param action action to send
+ */
+ public synchronized void sendAction(Action action) {
+ DelayedUpdate delayed = new DelayedUpdate(action);
+ delayedUpdateThreadPool.execute(delayed);
+ }
+
+ private void doSendAction(Action action) {
+ final @Nullable PrintStream printStream = this.printStream;
+ if (printStream != null) {
+ try {
+ printStream.println(outputMapper.writeValueAsString(action));
+ } catch (IOException e) {
+ logger.debug("Error while sending action '{}' to pilight server", action.getAction(), e);
+ }
+ } else {
+ logger.debug("Cannot send action '{}', not connected to pilight!", action.getAction());
+ }
+ }
+
+ /**
+ * Simple thread to allow calls to pilight to be throttled
+ */
+ private class DelayedUpdate implements Runnable {
+
+ private final Action action;
+
+ public DelayedUpdate(Action action) {
+ this.action = action;
+ }
+
+ @Override
+ public void run() {
+ long delayBetweenUpdates = config.getDelay();
+
+ long diff = new Date().getTime() - lastUpdate.getTime();
+ if (diff < delayBetweenUpdates) {
+ long delay = Math.min(delayBetweenUpdates - diff, config.getDelay());
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException e) {
+ logger.debug("Error while processing pilight throttling delay");
+ }
+ }
+
+ lastUpdate = new Date();
+ doSendAction(action);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBindingConstants.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBindingConstants.java
new file mode 100644
index 0000000000000..8316cc658e0de
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBindingConstants.java
@@ -0,0 +1,45 @@
+/**
+ * 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.pilight.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link PilightBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightBindingConstants {
+
+ public static final String BINDING_ID = "pilight";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
+ public static final ThingTypeUID THING_TYPE_CONTACT = new ThingTypeUID(BINDING_ID, "contact");
+ public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
+ public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
+ public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, "generic");
+
+ // List of property names
+ public static final String PROPERTY_IP_ADDRESS = "ipAddress";
+ public static final String PROPERTY_PORT = "port";
+ public static final String PROPERTY_NAME = "name";
+
+ // List of all Channel ids
+ public static final String CHANNEL_STATE = "state";
+ public static final String CHANNEL_DIMLEVEL = "dimlevel";
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBridgeConfiguration.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBridgeConfiguration.java
new file mode 100644
index 0000000000000..568f1e6a22bba
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBridgeConfiguration.java
@@ -0,0 +1,53 @@
+/**
+ * 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.pilight.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PilightBridgeConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightBridgeConfiguration {
+
+ private String ipAddress = "";
+ private Integer port = 0;
+ private Integer delay = 500;
+
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ public void setIpAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ public Integer getPort() {
+ return port;
+ }
+
+ public void setPort(Integer port) {
+ this.port = port;
+ }
+
+ public Integer getDelay() {
+ return delay;
+ }
+
+ public void setDelay(Integer delay) {
+ this.delay = delay;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightChannelConfiguration.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightChannelConfiguration.java
new file mode 100644
index 0000000000000..6416899a99afb
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightChannelConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * 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.pilight.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PilightChannelConfiguration} class contains fields mapping channel configuration parameters.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightChannelConfiguration {
+ private String property = "";
+
+ public String getProperty() {
+ return property;
+ }
+
+ public void setProperty(String property) {
+ this.property = property;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightDeviceConfiguration.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightDeviceConfiguration.java
new file mode 100644
index 0000000000000..0eeb6f4aca51f
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightDeviceConfiguration.java
@@ -0,0 +1,35 @@
+/**
+ * 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.pilight.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PilightDeviceConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightDeviceConfiguration {
+
+ private String name = "";
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightHandlerFactory.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightHandlerFactory.java
new file mode 100644
index 0000000000000..8a03ebea2cdfa
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightHandlerFactory.java
@@ -0,0 +1,92 @@
+/**
+ * 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.pilight.internal;
+
+import static org.openhab.binding.pilight.internal.PilightBindingConstants.*;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.handler.PilightBridgeHandler;
+import org.openhab.binding.pilight.internal.handler.PilightContactHandler;
+import org.openhab.binding.pilight.internal.handler.PilightDimmerHandler;
+import org.openhab.binding.pilight.internal.handler.PilightGenericHandler;
+import org.openhab.binding.pilight.internal.handler.PilightSwitchHandler;
+import org.openhab.core.thing.Bridge;
+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.openhab.core.thing.type.ChannelTypeRegistry;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link PilightHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.pilight", service = ThingHandlerFactory.class)
+public class PilightHandlerFactory extends BaseThingHandlerFactory {
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Stream
+ .of(THING_TYPE_BRIDGE, THING_TYPE_CONTACT, THING_TYPE_DIMMER, THING_TYPE_GENERIC, THING_TYPE_SWITCH)
+ .collect(Collectors.toSet());
+
+ private final ChannelTypeRegistry channelTypeRegistry;
+
+ @Activate
+ public PilightHandlerFactory(@Reference ChannelTypeRegistry channelTypeRegistry) {
+ this.channelTypeRegistry = channelTypeRegistry;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
+ return new PilightBridgeHandler((Bridge) thing);
+ }
+
+ if (THING_TYPE_CONTACT.equals(thingTypeUID)) {
+ return new PilightContactHandler(thing);
+ }
+
+ if (THING_TYPE_DIMMER.equals(thingTypeUID)) {
+ return new PilightDimmerHandler(thing);
+ }
+
+ if (THING_TYPE_GENERIC.equals(thingTypeUID)) {
+ return new PilightGenericHandler(thing, channelTypeRegistry);
+ }
+
+ if (THING_TYPE_SWITCH.equals(thingTypeUID)) {
+ return new PilightSwitchHandler(thing);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightBridgeDiscoveryService.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightBridgeDiscoveryService.java
new file mode 100644
index 0000000000000..cd3c405618a58
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightBridgeDiscoveryService.java
@@ -0,0 +1,158 @@
+/**
+ * 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.pilight.internal.discovery;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.PilightBindingConstants;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+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 PilightBridgeDiscoveryService} is responsible for discovering new pilight daemons on the network
+ * by sending a ssdp multicast request via udp.
+ *
+ * @author Niklas Dörfler - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.pilight")
+public class PilightBridgeDiscoveryService extends AbstractDiscoveryService {
+
+ private static final int AUTODISCOVERY_SEARCH_TIME = 5; // in seconds
+ private static final int AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL = 60 * 10; // in seconds
+
+ private static final String SSDP_DISCOVERY_REQUEST_MESSAGE = "M-SEARCH * HTTP/1.1\r\n"
+ + "Host:239.255.255.250:1900\r\n" + "ST:urn:schemas-upnp-org:service:pilight:1\r\n"
+ + "Man:\"ssdp:discover\"\r\n" + "MX:3\r\n\r\n";
+ public static final String SSDP_MULTICAST_ADDRESS = "239.255.255.250";
+ public static final int SSDP_PORT = 1900;
+ public static final int SSDP_WAIT_TIMEOUT = 2000; // in milliseconds
+
+ private final Logger logger = LoggerFactory.getLogger(PilightBridgeDiscoveryService.class);
+
+ private @Nullable ScheduledFuture> backgroundDiscoveryJob;
+
+ public PilightBridgeDiscoveryService() throws IllegalArgumentException {
+ super(getSupportedThingTypeUIDs(), AUTODISCOVERY_SEARCH_TIME, true);
+ }
+
+ public static Set getSupportedThingTypeUIDs() {
+ return Collections.singleton(PilightBindingConstants.THING_TYPE_BRIDGE);
+ }
+
+ @Override
+ protected void startScan() {
+ logger.debug("Pilight bridge discovery scan started");
+ removeOlderResults(getTimestampOfLastScan());
+ try {
+ List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+ for (NetworkInterface nic : interfaces) {
+ Enumeration inetAddresses = nic.getInetAddresses();
+ for (InetAddress inetAddress : Collections.list(inetAddresses)) {
+ if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
+ DatagramSocket ssdp = new DatagramSocket(
+ new InetSocketAddress(inetAddress.getHostAddress(), 0));
+ byte[] buff = SSDP_DISCOVERY_REQUEST_MESSAGE.getBytes();
+ DatagramPacket sendPack = new DatagramPacket(buff, buff.length);
+ sendPack.setAddress(InetAddress.getByName(SSDP_MULTICAST_ADDRESS));
+ sendPack.setPort(SSDP_PORT);
+ ssdp.send(sendPack);
+ ssdp.setSoTimeout(SSDP_WAIT_TIMEOUT);
+
+ boolean loop = true;
+ while (loop) {
+ DatagramPacket recvPack = new DatagramPacket(new byte[1024], 1024);
+ ssdp.receive(recvPack);
+ byte[] recvData = recvPack.getData();
+ InputStreamReader recvInput = new InputStreamReader(new ByteArrayInputStream(recvData),
+ StandardCharsets.UTF_8);
+ StringBuilder recvOutput = new StringBuilder();
+ for (int value; (value = recvInput.read()) != -1;) {
+ recvOutput.append((char) value);
+ }
+ BufferedReader bufReader = new BufferedReader(new StringReader(recvOutput.toString()));
+ Pattern pattern = Pattern.compile("Location:([0-9.]+):(.*)");
+ String line;
+ while ((line = bufReader.readLine()) != null) {
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.matches()) {
+ String server = matcher.group(1);
+ Integer port = Integer.parseInt(matcher.group(2));
+ loop = false;
+ logger.debug("Found pilight daemon at {}:{}", server, port);
+
+ Map properties = new HashMap<>(2);
+ properties.put(PilightBindingConstants.PROPERTY_IP_ADDRESS, server);
+ properties.put(PilightBindingConstants.PROPERTY_PORT, port);
+
+ ThingUID uid = new ThingUID(PilightBindingConstants.THING_TYPE_BRIDGE,
+ server.replace(".", "") + "" + port);
+
+ DiscoveryResult result = DiscoveryResultBuilder.create(uid)
+ .withProperties(properties).withLabel("Pilight Bridge (" + server + ")")
+ .build();
+
+ thingDiscovered(result);
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ if (!e.getMessage().equals("Receive timed out")) {
+ logger.warn("Unable to enumerate the local network interfaces {}", e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ protected synchronized void stopScan() {
+ super.stopScan();
+ removeOlderResults(getTimestampOfLastScan());
+ }
+
+ @Override
+ protected void startBackgroundDiscovery() {
+ logger.debug("Start Pilight device background discovery");
+ if (backgroundDiscoveryJob == null || backgroundDiscoveryJob.isCancelled()) {
+ backgroundDiscoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 5,
+ AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL, TimeUnit.SECONDS);
+ }
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ logger.debug("Stop Pilight device background discovery");
+ if (backgroundDiscoveryJob != null) {
+ backgroundDiscoveryJob.cancel(true);
+ backgroundDiscoveryJob = null;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightDeviceDiscoveryService.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightDeviceDiscoveryService.java
new file mode 100644
index 0000000000000..f5a9512d9485c
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightDeviceDiscoveryService.java
@@ -0,0 +1,231 @@
+/**
+ * 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.pilight.internal.discovery;
+
+import static org.openhab.binding.pilight.internal.PilightBindingConstants.*;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.PilightHandlerFactory;
+import org.openhab.binding.pilight.internal.dto.Config;
+import org.openhab.binding.pilight.internal.dto.DeviceType;
+import org.openhab.binding.pilight.internal.dto.Status;
+import org.openhab.binding.pilight.internal.handler.PilightBridgeHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PilightDeviceDiscoveryService} discovers pilight devices after a bridge thing has been created and
+ * connected to the pilight daemon. Things are discovered periodically in the background or after a manual trigger.
+ *
+ * @author Niklas Dörfler - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.pilight")
+public class PilightDeviceDiscoveryService extends AbstractDiscoveryService
+ implements DiscoveryService, ThingHandlerService {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet<>(
+ PilightHandlerFactory.SUPPORTED_THING_TYPES_UIDS);
+
+ private static final int AUTODISCOVERY_SEARCH_TIME = 10; // in seconds
+ private static final int AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL = 60 * 10; // in seconds
+
+ private final Logger logger = LoggerFactory.getLogger(PilightDeviceDiscoveryService.class);
+
+ private @Nullable PilightBridgeHandler pilightBridgeHandler;
+ private @Nullable ThingUID bridgeUID;
+
+ private @Nullable ScheduledFuture> backgroundDiscoveryJob;
+ private CompletableFuture configFuture;
+ private CompletableFuture> statusFuture;
+
+ public PilightDeviceDiscoveryService() throws IllegalArgumentException {
+ super(SUPPORTED_THING_TYPES_UIDS, AUTODISCOVERY_SEARCH_TIME);
+ configFuture = new CompletableFuture<>();
+ statusFuture = new CompletableFuture<>();
+ }
+
+ @Override
+ protected void startScan() {
+ if (pilightBridgeHandler != null) {
+ configFuture = new CompletableFuture<>();
+ statusFuture = new CompletableFuture<>();
+
+ CompletableFuture.allOf(configFuture, statusFuture).thenAccept(unused -> {
+ @Nullable
+ Config config = null;
+ @Nullable
+ List allStatus = null;
+
+ try {
+ config = configFuture.get();
+ allStatus = statusFuture.get();
+ } catch (InterruptedException | ExecutionException ignored) {
+ }
+
+ if (config != null && allStatus != null) {
+ removeOlderResults(getTimestampOfLastScan(), bridgeUID);
+ List finalAllStatus = allStatus;
+ config.getDevices().forEach((deviceId, device) -> {
+ if (this.pilightBridgeHandler != null) {
+ final @Nullable Status status = finalAllStatus.stream()
+ .filter(s -> deviceId.equals(s.getDevices().get(0))).findFirst().orElse(null);
+
+ final ThingTypeUID thingTypeUID;
+ final String typeString;
+
+ if (status != null) {
+ if (status.getType().equals(DeviceType.SWITCH)) {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_SWITCH.getId());
+ typeString = "Switch";
+ } else if (status.getType().equals(DeviceType.DIMMER)) {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_DIMMER.getId());
+ typeString = "Dimmer";
+ } else if (status.getType().equals(DeviceType.VALUE)) {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_GENERIC.getId());
+ typeString = "Generic";
+ } else if (status.getType().equals(DeviceType.CONTACT)) {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_CONTACT.getId());
+ typeString = "Contact";
+ } else {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_GENERIC.getId());
+ typeString = "Generic";
+ }
+ } else {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_GENERIC.getId());
+ typeString = "Generic";
+ }
+
+ final @Nullable ThingUID thingUID = new ThingUID(thingTypeUID,
+ this.pilightBridgeHandler.getThing().getUID(), deviceId);
+
+ Map properties = new HashMap<>();
+ properties.put(PROPERTY_NAME, deviceId);
+
+ DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+ .withThingType(thingTypeUID).withProperties(properties).withBridge(bridgeUID)
+ .withLabel("Pilight " + typeString + " Device '" + deviceId + "'").build();
+
+ thingDiscovered(discoveryResult);
+ }
+ });
+ }
+ });
+
+ pilightBridgeHandler.refreshConfigAndStatus();
+ }
+ }
+
+ @Override
+ protected synchronized void stopScan() {
+ super.stopScan();
+ if (bridgeUID != null) {
+ removeOlderResults(getTimestampOfLastScan(), bridgeUID);
+ }
+ }
+
+ @Override
+ protected void startBackgroundDiscovery() {
+ logger.debug("Start Pilight device background discovery");
+ if (backgroundDiscoveryJob == null || backgroundDiscoveryJob.isCancelled()) {
+ backgroundDiscoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 5,
+ AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL, TimeUnit.SECONDS);
+ }
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ logger.debug("Stop Pilight device background discovery");
+ if (backgroundDiscoveryJob != null) {
+ backgroundDiscoveryJob.cancel(true);
+ backgroundDiscoveryJob = null;
+ }
+ }
+
+ @Override
+ public void setThingHandler(@NonNullByDefault({}) ThingHandler handler) {
+ if (handler instanceof PilightBridgeHandler) {
+ pilightBridgeHandler = (PilightBridgeHandler) handler;
+ bridgeUID = pilightBridgeHandler.getThing().getUID();
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return pilightBridgeHandler;
+ }
+
+ @Override
+ public void activate() {
+ super.activate(null);
+ if (pilightBridgeHandler != null) {
+ pilightBridgeHandler.registerDiscoveryListener(this);
+ }
+ }
+
+ @Override
+ public void deactivate() {
+ if (bridgeUID != null) {
+ removeOlderResults(getTimestampOfLastScan(), bridgeUID);
+ }
+
+ if (pilightBridgeHandler != null) {
+ pilightBridgeHandler.unregisterDiscoveryListener();
+ }
+
+ super.deactivate();
+ }
+
+ /**
+ * Method used to get pilight device config into the discovery class.
+ *
+ * @param config config to get
+ */
+ public void setConfig(Config config) {
+ if (!configFuture.isDone()) {
+ configFuture.complete(config);
+ }
+ }
+
+ /**
+ * Method used to get pilight device status list into the discovery class.
+ *
+ * @param status list of status objects
+ */
+ public void setStatus(List status) {
+ if (!statusFuture.isDone()) {
+ statusFuture.complete(status);
+ }
+ }
+
+ @Override
+ public Set getSupportedThingTypes() {
+ return SUPPORTED_THING_TYPES_UIDS;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Action.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Action.java
new file mode 100644
index 0000000000000..a94a8f78c9eb9
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Action.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.pilight.internal.dto;
+
+/**
+ * This message is sent when we want to change the state of a device or request the
+ * current configuration in pilight.
+ *
+ * @author Jeroen Idserda - Initial contribution
+ * @author Stefan Röllin - Port to openHAB 2 pilight binding
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+public class Action {
+
+ public static final String ACTION_SEND = "send";
+
+ public static final String ACTION_CONTROL = "control";
+
+ public static final String ACTION_REQUEST_CONFIG = "request config";
+
+ public static final String ACTION_REQUEST_VALUES = "request values";
+
+ private String action;
+
+ private Code code;
+
+ private Options options;
+
+ public Action(String action) {
+ this.action = action;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public Code getCode() {
+ return code;
+ }
+
+ public void setCode(Code code) {
+ this.code = code;
+ }
+
+ public Options getOptions() {
+ return options;
+ }
+
+ public void setOptions(Options options) {
+ this.options = options;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/AllStatus.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/AllStatus.java
new file mode 100644
index 0000000000000..933a17c6aa224
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/AllStatus.java
@@ -0,0 +1,45 @@
+/**
+ * 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.pilight.internal.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * All status messages.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+public class AllStatus {
+
+ private String message;
+
+ private List values = new ArrayList<>();
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public List getValues() {
+ return values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Code.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Code.java
new file mode 100644
index 0000000000000..029a2370deb9c
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Code.java
@@ -0,0 +1,60 @@
+/**
+ * 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.pilight.internal.dto;
+
+/**
+ * Part of the {@link Action} message that is sent to pilight.
+ * This contains the desired state for a single device.
+ *
+ * {@link http://www.pilight.org/development/api/#sender}
+ *
+ * @author Jeroen Idserda - Initial contribution
+ * @author Stefan Röllin - Port to openHAB 2 pilight binding
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+public class Code {
+
+ public static final String STATE_ON = "on";
+
+ public static final String STATE_OFF = "off";
+
+ private String device;
+
+ private String state;
+
+ private Values values;
+
+ public String getDevice() {
+ return device;
+ }
+
+ public void setDevice(String device) {
+ this.device = device;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public Values getValues() {
+ return values;
+ }
+
+ public void setValues(Values values) {
+ this.values = values;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Config.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Config.java
new file mode 100644
index 0000000000000..d20a8313785c8
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Config.java
@@ -0,0 +1,40 @@
+/**
+ * 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.pilight.internal.dto;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * pilight configuration object
+ *
+ * {@link http://www.pilight.org/development/api/#controller}
+ *
+ * @author Jeroen Idserda - Initial contribution
+ * @author Stefan Röllin - Port to openHAB 2 pilight binding
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Config {
+
+ private Map devices;
+
+ public Map getDevices() {
+ return devices;
+ }
+
+ public void setDevices(Map devices) {
+ this.devices = devices;
+ }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Device.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Device.java
new file mode 100644
index 0000000000000..90499f68a2c73
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Device.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.pilight.internal.dto;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Class describing a device in pilight
+ *
+ * @author Jeroen Idserda - Initial contribution
+ * @author Stefan Röllin - Port to openHAB 2 pilight binding
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Device {
+
+ private String uuid;
+
+ private String origin;
+
+ private String timestamp;
+
+ private List protocol;
+
+ private String state;
+
+ private Integer dimlevel = null;
+
+ // @SerializedName("dimlevel-maximum")
+ private Integer dimlevelMaximum = null;
+
+ private Integer dimlevelMinimum = null;
+
+ private List