diff --git a/CODEOWNERS b/CODEOWNERS
index e08a8885edf26..5a94b11062ae5 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -208,6 +208,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 37bf387899738..c7dbcefbccc2c 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1026,6 +1026,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..84d35ac52ac16
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/README.md
@@ -0,0 +1,119 @@
+# 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
+
+### `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 all 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, so 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.
+
+## Full Example
+
+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..1acde190f7394
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 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
+
+
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..38a968d3f2ba8
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ 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..9caa84f3f976f
--- /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-2021 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/PilightBindingConstants.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBindingConstants.java
new file mode 100644
index 0000000000000..bcf731315b83c
--- /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-2021 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..85dce4004946f
--- /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-2021 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 int port = 0;
+ private int 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 int 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..594eff9d77088
--- /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-2021 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/PilightConnector.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightConnector.java
new file mode 100644
index 0000000000000..75fca4f9cf4e2
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightConnector.java
@@ -0,0 +1,264 @@
+/**
+ * Copyright (c) 2010-2021 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.*;
+import java.net.Socket;
+import java.util.Collections;
+import java.util.concurrent.*;
+
+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 implements Runnable, Closeable {
+
+ private static final int 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 @Nullable Socket socket;
+ private @Nullable PrintStream printStream;
+
+ private final ScheduledExecutorService scheduler;
+ private final ConcurrentLinkedQueue delayedActionQueue = new ConcurrentLinkedQueue<>();
+ private @Nullable ScheduledFuture> delayedActionWorkerFuture;
+
+ public PilightConnector(final PilightBridgeConfiguration config, final IPilightCallback callback,
+ final ScheduledExecutorService scheduler) {
+ this.config = config;
+ this.callback = callback;
+ this.scheduler = scheduler;
+ }
+
+ @Override
+ public void run() {
+ try {
+ connect();
+
+ while (!Thread.currentThread().isInterrupted()) {
+ try {
+ final @Nullable Socket socket = this.socket;
+ if (socket != null && !socket.isClosed()) {
+ try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
+ String line = in.readLine();
+ while (!Thread.currentThread().isInterrupted() && line != null) {
+ if (!line.isEmpty()) {
+ logger.trace("Received from pilight: {}", line);
+ final ObjectMapper inputMapper = this.inputMapper;
+ if (line.startsWith("{\"message\":\"config\"")) {
+ final @Nullable Message message = inputMapper.readValue(line, Message.class);
+ callback.configReceived(message.getConfig());
+ } else if (line.startsWith("{\"message\":\"values\"")) {
+ final @Nullable AllStatus status = inputMapper.readValue(line, AllStatus.class);
+ callback.statusReceived(status.getValues());
+ } else if (line.startsWith("{\"version\":")) {
+ final @Nullable Version version = inputMapper.readValue(line, Version.class);
+ callback.versionReceived(version);
+ } else if (line.startsWith("{\"status\":")) {
+ // currently unused
+ } else if (line.equals("1")) {
+ throw new IOException("Connection to pilight lost");
+ } else {
+ final @Nullable Status status = inputMapper.readValue(line, Status.class);
+ callback.statusReceived(Collections.singletonList(status));
+ }
+ }
+
+ line = in.readLine();
+ }
+ }
+ }
+ } catch (IOException e) {
+ if (!Thread.currentThread().isInterrupted()) {
+ logger.debug("Error in pilight listener thread: {}", e.getMessage());
+ }
+ }
+
+ logger.debug("Disconnected from pilight server at {}:{}", config.getIpAddress(), config.getPort());
+
+ if (!Thread.currentThread().isInterrupted()) {
+ callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, null);
+ // empty line received (socket closed) or pilight stopped but binding
+ // is still running, try to reconnect
+ connect();
+ }
+ }
+
+ } catch (InterruptedException e) {
+ logger.debug("Interrupting thread.");
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Tells the connector to refresh the configuration
+ */
+ public void refreshConfig() {
+ doSendAction(new Action(Action.ACTION_REQUEST_CONFIG));
+ }
+
+ /**
+ * Tells the connector to refresh the status of all devices
+ */
+ public void refreshStatus() {
+ doSendAction(new Action(Action.ACTION_REQUEST_VALUES));
+ }
+
+ /**
+ * Stops the listener
+ */
+ public void close() {
+ disconnect();
+ Thread.currentThread().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.getMessage());
+ }
+ this.socket = null;
+ }
+ }
+
+ private boolean isConnected() {
+ final @Nullable Socket socket = this.socket;
+ return socket != null && !socket.isClosed();
+ }
+
+ private void connect() throws InterruptedException {
+ 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));
+
+ final @Nullable Response response = inputMapper.readValue(socket.getInputStream(), Response.class);
+
+ if (response.getStatus().equals(Response.SUCCESS)) {
+ logger.debug("Established connection to pilight server at {}:{}", config.getIpAddress(),
+ config.getPort());
+ this.socket = socket;
+ this.printStream = printStream;
+ callback.updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
+ } else {
+ printStream.close();
+ socket.close();
+ logger.debug("pilight client not accepted: {}", response.getStatus());
+ }
+ } catch (IOException e) {
+ final @Nullable PrintStream printStream = this.printStream;
+ if (printStream != null) {
+ printStream.close();
+ }
+ logger.debug("connect failed: {}", e.getMessage());
+ callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+
+ delay = RECONNECT_DELAY_MSEC;
+ }
+ }
+
+ /**
+ * send action to pilight daemon
+ *
+ * @param action action to send
+ */
+ public void sendAction(Action action) {
+ delayedActionQueue.add(action);
+ final @Nullable ScheduledFuture> delayedActionWorkerFuture = this.delayedActionWorkerFuture;
+
+ if (delayedActionWorkerFuture == null || delayedActionWorkerFuture.isCancelled()) {
+ this.delayedActionWorkerFuture = scheduler.scheduleWithFixedDelay(() -> {
+ if (!delayedActionQueue.isEmpty()) {
+ doSendAction(delayedActionQueue.poll());
+ } else {
+ final @Nullable ScheduledFuture> workerFuture = this.delayedActionWorkerFuture;
+ if (workerFuture != null) {
+ workerFuture.cancel(false);
+ }
+ this.delayedActionWorkerFuture = null;
+ }
+ }, 0, config.getDelay(), TimeUnit.MILLISECONDS);
+ }
+ }
+
+ 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.getMessage());
+ }
+ } else {
+ logger.debug("Cannot send action '{}', not connected to pilight!", action.getAction());
+ }
+ }
+}
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..b5cbff1f000ab
--- /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-2021 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..366b2e4b21de8
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightHandlerFactory.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2010-2021 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 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 = Set.of(THING_TYPE_BRIDGE, THING_TYPE_CONTACT,
+ THING_TYPE_DIMMER, THING_TYPE_GENERIC, THING_TYPE_SWITCH);
+
+ 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..67cb46bd87b6e
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightBridgeDiscoveryService.java
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2010-2021 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 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_SEC = 5;
+ private static final int AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC = 60 * 10;
+
+ 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_SEC, 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(StandardCharsets.UTF_8);
+ 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();
+
+ final Scanner scanner = new Scanner(new ByteArrayInputStream(recvData),
+ StandardCharsets.UTF_8);
+ loop = scanner.findAll("Location:([0-9.]+):(.*)").peek(matchResult -> {
+ final String server = matchResult.group(1);
+ final Integer port = Integer.parseInt(matchResult.group(2));
+ final String bridgeName = server.replace(".", "") + "" + port;
+
+ logger.debug("Found pilight daemon at {}:{}", server, port);
+
+ Map properties = new HashMap<>();
+ properties.put(PilightBindingConstants.PROPERTY_IP_ADDRESS, server);
+ properties.put(PilightBindingConstants.PROPERTY_PORT, port);
+ properties.put(PilightBindingConstants.PROPERTY_NAME, bridgeName);
+
+ ThingUID uid = new ThingUID(PilightBindingConstants.THING_TYPE_BRIDGE, bridgeName);
+
+ DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
+ .withRepresentationProperty(PilightBindingConstants.PROPERTY_NAME)
+ .withLabel("Pilight Bridge (" + server + ")").build();
+
+ thingDiscovered(result);
+ }).count() == 0;
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ if (e.getMessage() != null && !"Receive timed out".equals(e.getMessage())) {
+ 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");
+ final @Nullable ScheduledFuture> backgroundDiscoveryJob = this.backgroundDiscoveryJob;
+ if (backgroundDiscoveryJob == null || backgroundDiscoveryJob.isCancelled()) {
+ this.backgroundDiscoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 5,
+ AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC, TimeUnit.SECONDS);
+ }
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ logger.debug("Stop Pilight device background discovery");
+ final @Nullable ScheduledFuture> backgroundDiscoveryJob = this.backgroundDiscoveryJob;
+ if (backgroundDiscoveryJob != null) {
+ backgroundDiscoveryJob.cancel(true);
+ this.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..c735a976e36a5
--- /dev/null
+++ b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightDeviceDiscoveryService.java
@@ -0,0 +1,223 @@
+/**
+ * Copyright (c) 2010-2021 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.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.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+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
+public class PilightDeviceDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = PilightHandlerFactory.SUPPORTED_THING_TYPES_UIDS;
+
+ private static final int AUTODISCOVERY_SEARCH_TIME_SEC = 10;
+ private static final int AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC = 60 * 10;
+
+ 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() {
+ super(SUPPORTED_THING_TYPES_UIDS, AUTODISCOVERY_SEARCH_TIME_SEC);
+ configFuture = new CompletableFuture<>();
+ statusFuture = new CompletableFuture<>();
+ }
+
+ @Override
+ protected void startScan() {
+ if (pilightBridgeHandler != null) {
+ configFuture = new CompletableFuture<>();
+ statusFuture = new CompletableFuture<>();
+
+ configFuture.thenAcceptBoth(statusFuture, (config, allStatus) -> {
+ removeOlderResults(getTimestampOfLastScan(), bridgeUID);
+ config.getDevices().forEach((deviceId, device) -> {
+ if (this.pilightBridgeHandler != null) {
+ final Optional status = allStatus.stream()
+ .filter(s -> s.getDevices().contains(deviceId)).findFirst();
+
+ final ThingTypeUID thingTypeUID;
+ final String typeString;
+
+ if (status.isPresent()) {
+ if (status.get().getType().equals(DeviceType.SWITCH)) {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_SWITCH.getId());
+ typeString = "Switch";
+ } else if (status.get().getType().equals(DeviceType.DIMMER)) {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_DIMMER.getId());
+ typeString = "Dimmer";
+ } else if (status.get().getType().equals(DeviceType.VALUE)) {
+ thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_GENERIC.getId());
+ typeString = "Generic";
+ } else if (status.get().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 PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler;
+ if (pilightBridgeHandler != null) {
+ final ThingUID thingUID = new ThingUID(thingTypeUID,
+ pilightBridgeHandler.getThing().getUID(), deviceId);
+
+ final Map properties = new HashMap<>();
+ properties.put(PROPERTY_NAME, deviceId);
+
+ DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+ .withThingType(thingTypeUID).withProperties(properties).withBridge(bridgeUID)
+ .withRepresentationProperty(PROPERTY_NAME)
+ .withLabel("Pilight " + typeString + " Device '" + deviceId + "'").build();
+
+ thingDiscovered(discoveryResult);
+ }
+ }
+ });
+ });
+
+ final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler;
+ if (pilightBridgeHandler != null) {
+ pilightBridgeHandler.refreshConfigAndStatus();
+ }
+ }
+ }
+
+ @Override
+ protected synchronized void stopScan() {
+ super.stopScan();
+ configFuture.cancel(true);
+ statusFuture.cancel(true);
+ if (bridgeUID != null) {
+ removeOlderResults(getTimestampOfLastScan(), bridgeUID);
+ }
+ }
+
+ @Override
+ protected void startBackgroundDiscovery() {
+ logger.debug("Start Pilight device background discovery");
+ final @Nullable ScheduledFuture> backgroundDiscoveryJob = this.backgroundDiscoveryJob;
+ if (backgroundDiscoveryJob == null || backgroundDiscoveryJob.isCancelled()) {
+ this.backgroundDiscoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 20,
+ AUTODISCOVERY_BACKGROUND_SEARCH_INTERVAL_SEC, TimeUnit.SECONDS);
+ }
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ logger.debug("Stop Pilight device background discovery");
+ final @Nullable ScheduledFuture> backgroundDiscoveryJob = this.backgroundDiscoveryJob;
+ if (backgroundDiscoveryJob != null) {
+ backgroundDiscoveryJob.cancel(true);
+ this.backgroundDiscoveryJob = null;
+ }
+ }
+
+ @Override
+ public void setThingHandler(final ThingHandler handler) {
+ if (handler instanceof PilightBridgeHandler) {
+ this.pilightBridgeHandler = (PilightBridgeHandler) handler;
+ final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler;
+ if (pilightBridgeHandler != null) {
+ bridgeUID = pilightBridgeHandler.getThing().getUID();
+ }
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return pilightBridgeHandler;
+ }
+
+ @Override
+ public void activate() {
+ super.activate(null);
+ final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler;
+ if (pilightBridgeHandler != null) {
+ pilightBridgeHandler.registerDiscoveryListener(this);
+ }
+ }
+
+ @Override
+ public void deactivate() {
+ if (bridgeUID != null) {
+ removeOlderResults(getTimestampOfLastScan(), bridgeUID);
+ }
+
+ final @Nullable PilightBridgeHandler pilightBridgeHandler = this.pilightBridgeHandler;
+ 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) {
+ 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) {
+ 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..ed0a9259295db
--- /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-2021 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..69418aa581ea0
--- /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-2021 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..9cc253bc1407e
--- /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-2021 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..6f368a62f1d4d
--- /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-2021 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..62c6c26cdcdb4
--- /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-2021 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