]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pilight] Pilight Binding initial contribution (#9744)
authorNiklas Dörfler <niklas@doerfler-el.de>
Wed, 17 Feb 2021 18:59:54 +0000 (19:59 +0100)
committerGitHub <noreply@github.com>
Wed, 17 Feb 2021 18:59:54 +0000 (19:59 +0100)
Signed-off-by: Niklas Dörfler <niklas@doerfler-el.de>
42 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.pilight/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.pilight/README.md [new file with mode: 0644]
bundles/org.openhab.binding.pilight/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/IPilightCallback.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightChannelConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightDeviceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/PilightHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightBridgeDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/discovery/PilightDeviceDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Action.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/AllStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Code.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Config.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Device.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/DeviceType.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Identification.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Message.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Options.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Response.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Status.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Values.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Version.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBaseHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightContactHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightDimmerHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightGenericHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightSwitchHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/serializers/BooleanToIntegerSerializer.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/types/PilightContactType.java [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/i18n/pilight_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/bridge.xml [new file with mode: 0644]
bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/devices.xml [new file with mode: 0644]
bundles/pom.xml

index e08a8885edf26e920beafbc07658f3e385fd82d3..5a94b11062ae57d1f7de6b639dc565b1b7003b98 100644 (file)
 /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
index 37bf38789973805a4721a3758f6b9b23ccfe1dbc..c7dbcefbccc2c8e66a7e6e037ed60c5df1798487 100644 (file)
       <artifactId>org.openhab.binding.phc</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.pilight</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.pioneeravr</artifactId>
diff --git a/bundles/org.openhab.binding.pilight/NOTICE b/bundles/org.openhab.binding.pilight/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..84d35ac
--- /dev/null
@@ -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]" <temperature>  { channel="pilight:generic:raspi:weather:temperature" }
+Number weather_humidity "Feuchtigkeit [%.0f %%]" <humidity> { 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 (file)
index 0000000..1acde19
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.pilight</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Pilight Binding</name>
+
+</project>
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 (file)
index 0000000..38a968d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.pilight-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-pilight" description="Pilight Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pilight/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..9caa84f
--- /dev/null
@@ -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<Status> 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 (file)
index 0000000..bcf7313
--- /dev/null
@@ -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 (file)
index 0000000..85dce40
--- /dev/null
@@ -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 (file)
index 0000000..594eff9
--- /dev/null
@@ -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 (file)
index 0000000..75fca4f
--- /dev/null
@@ -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<Action> 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 (file)
index 0000000..b5cbff1
--- /dev/null
@@ -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 (file)
index 0000000..366b2e4
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..67cb46b
--- /dev/null
@@ -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<ThingTypeUID> getSupportedThingTypeUIDs() {
+        return Collections.singleton(PilightBindingConstants.THING_TYPE_BRIDGE);
+    }
+
+    @Override
+    protected void startScan() {
+        logger.debug("Pilight bridge discovery scan started");
+        removeOlderResults(getTimestampOfLastScan());
+        try {
+            List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+            for (NetworkInterface nic : interfaces) {
+                Enumeration<InetAddress> 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<String, Object> 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 (file)
index 0000000..c735a97
--- /dev/null
@@ -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<ThingTypeUID> 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<Config> configFuture;
+    private CompletableFuture<List<Status>> 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> 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<String, Object> 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> status) {
+        statusFuture.complete(status);
+    }
+
+    @Override
+    public Set<ThingTypeUID> 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 (file)
index 0000000..ed0a925
--- /dev/null
@@ -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 (file)
index 0000000..69418aa
--- /dev/null
@@ -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<Status> values = new ArrayList<>();
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public List<Status> getValues() {
+        return values;
+    }
+
+    public void setValues(List<Status> 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 (file)
index 0000000..9cc253b
--- /dev/null
@@ -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 (file)
index 0000000..6f368a6
--- /dev/null
@@ -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<String, Device> devices;
+
+    public Map<String, Device> getDevices() {
+        return devices;
+    }
+
+    public void setDevices(Map<String, Device> 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 (file)
index 0000000..62c6c26
--- /dev/null
@@ -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<String> protocol;
+
+    private String state;
+
+    private Integer dimlevel = null;
+
+    // @SerializedName("dimlevel-maximum")
+    private Integer dimlevelMaximum = null;
+
+    private Integer dimlevelMinimum = null;
+
+    private List<Map<String, String>> id;
+
+    private Map<String, String> properties = new HashMap<>();
+
+    public String getUuid() {
+        return uuid;
+    }
+
+    public void setUuid(String uuid) {
+        this.uuid = uuid;
+    }
+
+    public String getOrigin() {
+        return origin;
+    }
+
+    public void setOrigin(String origin) {
+        this.origin = origin;
+    }
+
+    public String getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(String timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public List<String> getProtocol() {
+        return protocol;
+    }
+
+    public void setProtocol(List<String> protocol) {
+        this.protocol = protocol;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+    public Integer getDimlevel() {
+        return dimlevel;
+    }
+
+    public void setDimlevel(Integer dimlevel) {
+        this.dimlevel = dimlevel;
+    }
+
+    public Integer getDimlevelMaximum() {
+        return dimlevelMaximum;
+    }
+
+    @JsonProperty("dimlevel-maximum")
+    public void setDimlevelMaximum(Integer dimlevelMaximum) {
+        this.dimlevelMaximum = dimlevelMaximum;
+    }
+
+    public Integer getDimlevelMinimum() {
+        return dimlevelMinimum;
+    }
+
+    @JsonProperty("dimlevel-minimum")
+    public void setDimlevelMinimum(Integer dimlevelMinimum) {
+        this.dimlevelMinimum = dimlevelMinimum;
+    }
+
+    public List<Map<String, String>> getId() {
+        return id;
+    }
+
+    public void setId(List<Map<String, String>> id) {
+        this.id = id;
+    }
+
+    public void setProperties(Map<String, String> properties) {
+        this.properties = properties;
+    }
+
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @JsonAnySetter
+    public void set(String name, Object value) {
+        properties.put(name, value.toString());
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/DeviceType.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/DeviceType.java
new file mode 100644 (file)
index 0000000..19a1929
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * 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;
+
+/**
+ * Different types of devices 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 DeviceType {
+
+    public static final Integer SERVER = -1;
+
+    public static final Integer SWITCH = 1;
+
+    public static final Integer DIMMER = 2;
+
+    public static final Integer VALUE = 3;
+
+    public static final Integer CONTACT = 6;
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Identification.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Identification.java
new file mode 100644 (file)
index 0000000..749492a
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * 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 org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This object is sent to pilight right after the initial connection. It describes what kind of client we want to be.
+ *
+ * @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 Identification {
+
+    public static final String ACTION_IDENTIFY = "identify";
+
+    private String action;
+
+    private Options options = new Options();
+
+    public Identification() {
+        this.action = ACTION_IDENTIFY;
+    }
+
+    public String getAction() {
+        return action;
+    }
+
+    public void setAction(String action) {
+        this.action = action;
+    }
+
+    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/Message.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Message.java
new file mode 100644 (file)
index 0000000..449f204
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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;
+
+/**
+ * Wrapper for the {@code Config} object
+ *
+ * @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 Message {
+
+    private Config config;
+
+    private String message;
+
+    public Config getConfig() {
+        return config;
+    }
+
+    public void setConfig(Config config) {
+        this.config = config;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Options.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Options.java
new file mode 100644 (file)
index 0000000..3f639c8
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * 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 org.openhab.binding.pilight.internal.serializers.BooleanToIntegerSerializer;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+/**
+ * Options that can be set as a pilight client.
+ *
+ * @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 Options {
+
+    public static final String MEDIA_ALL = "all";
+
+    public static final String MEDIA_WEB = "web";
+
+    public static final String MEDIA_MOBILE = "mobile";
+
+    public static final String MEDIA_DESKTOP = "desktop";
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @JsonSerialize(using = BooleanToIntegerSerializer.class)
+    private Boolean core;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @JsonSerialize(using = BooleanToIntegerSerializer.class)
+    private Boolean receiver;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @JsonSerialize(using = BooleanToIntegerSerializer.class)
+    private Boolean config;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @JsonSerialize(using = BooleanToIntegerSerializer.class)
+    private Boolean forward;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    @JsonSerialize(using = BooleanToIntegerSerializer.class)
+    private Boolean stats;
+
+    private String uuid;
+
+    private String media;
+
+    public Boolean getCore() {
+        return core;
+    }
+
+    public void setCore(Boolean core) {
+        this.core = core;
+    }
+
+    public Boolean getReceiver() {
+        return receiver;
+    }
+
+    public void setReceiver(Boolean receiver) {
+        this.receiver = receiver;
+    }
+
+    public Boolean getConfig() {
+        return config;
+    }
+
+    public void setConfig(Boolean config) {
+        this.config = config;
+    }
+
+    public Boolean getForward() {
+        return forward;
+    }
+
+    public void setForward(Boolean forward) {
+        this.forward = forward;
+    }
+
+    public Boolean getStats() {
+        return stats;
+    }
+
+    public void setStats(Boolean stats) {
+        this.stats = stats;
+    }
+
+    public String getUuid() {
+        return uuid;
+    }
+
+    public void setUuid(String uuid) {
+        this.uuid = uuid;
+    }
+
+    public String getMedia() {
+        return media;
+    }
+
+    public void setMedia(String media) {
+        this.media = media;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Response.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Response.java
new file mode 100644 (file)
index 0000000..bdde312
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * 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;
+
+/**
+ * Response to a connection or state change request
+ *
+ * @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 Response {
+
+    public static final String SUCCESS = "success";
+
+    public static final String FAILURE = "failure";
+
+    private String status;
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public boolean isSuccess() {
+        return SUCCESS.equals(status);
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Status.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Status.java
new file mode 100644 (file)
index 0000000..8c5c4a3
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * 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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A Status message is received when a device in pilight changes state.
+ *
+ * @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 Status {
+
+    private String origin;
+
+    private Integer type;
+
+    private String uuid;
+
+    private List<String> devices = new ArrayList<>();
+
+    private Map<String, String> values = new HashMap<>();
+
+    public Status() {
+    }
+
+    public String getOrigin() {
+        return origin;
+    }
+
+    public void setOrigin(String origin) {
+        this.origin = origin;
+    }
+
+    public Integer getType() {
+        return type;
+    }
+
+    public void setType(Integer type) {
+        this.type = type;
+    }
+
+    public String getUuid() {
+        return uuid;
+    }
+
+    public void setUuid(String uuid) {
+        this.uuid = uuid;
+    }
+
+    public List<String> getDevices() {
+        return devices;
+    }
+
+    public void setDevices(List<String> devices) {
+        this.devices = devices;
+    }
+
+    public Map<String, String> getValues() {
+        return values;
+    }
+
+    public void setValues(Map<String, String> values) {
+        this.values = values;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Values.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Values.java
new file mode 100644 (file)
index 0000000..65798b7
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * 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;
+
+/**
+ * Describes the specific properties of a device
+ *
+ * @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 Values {
+
+    private Integer dimlevel;
+
+    public Integer getDimlevel() {
+        return dimlevel;
+    }
+
+    public void setDimlevel(Integer dimlevel) {
+        this.dimlevel = dimlevel;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Version.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/dto/Version.java
new file mode 100644 (file)
index 0000000..755eff8
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * pilight version information object
+ *
+ * {@link http://www.pilight.org/development/api/#controller}
+ *
+ * @author Niklas Dörfler - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Version {
+
+    private String version;
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBaseHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBaseHandler.java
new file mode 100644 (file)
index 0000000..aeb1e48
--- /dev/null
@@ -0,0 +1,127 @@
+/**
+ * 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.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.PilightDeviceConfiguration;
+import org.openhab.binding.pilight.internal.dto.Action;
+import org.openhab.binding.pilight.internal.dto.Config;
+import org.openhab.binding.pilight.internal.dto.Device;
+import org.openhab.binding.pilight.internal.dto.Status;
+import org.openhab.core.thing.*;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PilightBaseHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public abstract class PilightBaseHandler extends BaseThingHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(PilightBaseHandler.class);
+
+    private String name = "";
+
+    public PilightBaseHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if (command instanceof RefreshType) {
+            refreshConfigAndStatus();
+            return;
+        }
+
+        @Nullable
+        Action action = createUpdateCommand(channelUID, command);
+        if (action != null) {
+            sendAction(action);
+        }
+    }
+
+    @Override
+    public void initialize() {
+        PilightDeviceConfiguration config = getConfigAs(PilightDeviceConfiguration.class);
+        name = config.getName();
+
+        refreshConfigAndStatus();
+    }
+
+    public void updateFromStatusIfMatches(Status status) {
+        if (status.getDevices() != null && !status.getDevices().isEmpty()) {
+            if (name.equals(status.getDevices().get(0))) {
+                if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
+                    updateStatus(ThingStatus.ONLINE);
+                }
+                updateFromStatus(status);
+            }
+        }
+    }
+
+    public void updateFromConfigIfMatches(Config config) {
+        Device device = config.getDevices().get(getName());
+        if (device != null) {
+            updateFromConfigDevice(device);
+        }
+    }
+
+    abstract void updateFromStatus(Status status);
+
+    abstract void updateFromConfigDevice(Device device);
+
+    abstract @Nullable Action createUpdateCommand(ChannelUID channelUID, Command command);
+
+    protected String getName() {
+        return name;
+    }
+
+    private void sendAction(Action action) {
+        final @Nullable PilightBridgeHandler handler = getPilightBridgeHandler();
+        if (handler != null) {
+            handler.sendAction(action);
+        } else {
+            logger.warn("No pilight bridge handler found to send action.");
+        }
+    }
+
+    private void refreshConfigAndStatus() {
+        final @Nullable PilightBridgeHandler handler = getPilightBridgeHandler();
+        if (handler != null) {
+            handler.refreshConfigAndStatus();
+        } else {
+            logger.warn("No pilight bridge handler found to refresh config and status.");
+        }
+    }
+
+    private @Nullable PilightBridgeHandler getPilightBridgeHandler() {
+        final @Nullable Bridge bridge = getBridge();
+        if (bridge != null) {
+            @Nullable
+            BridgeHandler handler = bridge.getHandler();
+            if (handler instanceof PilightBridgeHandler) {
+                return (PilightBridgeHandler) handler;
+            }
+        }
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBridgeHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightBridgeHandler.java
new file mode 100644 (file)
index 0000000..dfcfd9f
--- /dev/null
@@ -0,0 +1,233 @@
+/**
+ * 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.handler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+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.IPilightCallback;
+import org.openhab.binding.pilight.internal.PilightBridgeConfiguration;
+import org.openhab.binding.pilight.internal.PilightConnector;
+import org.openhab.binding.pilight.internal.discovery.PilightDeviceDiscoveryService;
+import org.openhab.binding.pilight.internal.dto.*;
+import org.openhab.core.common.NamedThreadFactory;
+import org.openhab.core.thing.*;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PilightBridgeHandler} is responsible dispatching commands for the child
+ * things to the Pilight daemon and sending status updates to the child things.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightBridgeHandler extends BaseBridgeHandler {
+
+    private static final int REFRESH_CONFIG_MSEC = 500;
+
+    private final Logger logger = LoggerFactory.getLogger(PilightBridgeHandler.class);
+
+    private @Nullable PilightConnector connector = null;
+
+    private @Nullable ScheduledFuture<?> refreshJob = null;
+
+    private @Nullable PilightDeviceDiscoveryService discoveryService = null;
+
+    private final ExecutorService connectorExecutor = Executors
+            .newSingleThreadExecutor(new NamedThreadFactory(getThing().getUID().getAsString(), true));
+
+    public PilightBridgeHandler(Bridge bridge) {
+        super(bridge);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("Pilight Bridge is read-only and does not handle commands.");
+    }
+
+    @Override
+    public void initialize() {
+        PilightBridgeConfiguration config = getConfigAs(PilightBridgeConfiguration.class);
+
+        final @Nullable PilightDeviceDiscoveryService discoveryService = this.discoveryService;
+        PilightConnector connector = new PilightConnector(config, new IPilightCallback() {
+            @Override
+            public void updateThingStatus(ThingStatus status, ThingStatusDetail statusDetail,
+                    @Nullable String description) {
+                updateStatus(status, statusDetail, description);
+                if (status == ThingStatus.ONLINE) {
+                    refreshConfigAndStatus();
+                }
+            }
+
+            @Override
+            public void statusReceived(List<Status> allStatus) {
+                for (Status status : allStatus) {
+                    processStatus(status);
+                }
+
+                if (discoveryService != null) {
+                    discoveryService.setStatus(allStatus);
+                }
+            }
+
+            @Override
+            public void configReceived(Config config) {
+                processConfig(config);
+            }
+
+            @Override
+            public void versionReceived(Version version) {
+                getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, version.getVersion());
+            }
+        }, scheduler);
+
+        updateStatus(ThingStatus.UNKNOWN);
+
+        connectorExecutor.execute(connector);
+        this.connector = connector;
+    }
+
+    @Override
+    public void dispose() {
+        final @Nullable ScheduledFuture<?> future = this.refreshJob;
+        if (future != null) {
+            future.cancel(true);
+        }
+
+        final @Nullable PilightConnector connector = this.connector;
+        if (connector != null) {
+            connector.close();
+            this.connector = null;
+        }
+
+        connectorExecutor.shutdown();
+    }
+
+    /**
+     * send action to pilight daemon
+     *
+     * @param action action to send
+     */
+    public void sendAction(Action action) {
+        final @Nullable PilightConnector connector = this.connector;
+        if (connector != null) {
+            connector.sendAction(action);
+        }
+    }
+
+    /**
+     * refresh config and status by requesting config and all values from pilight daemon
+     */
+    public synchronized void refreshConfigAndStatus() {
+        if (thing.getStatus() == ThingStatus.ONLINE) {
+            final @Nullable ScheduledFuture<?> refreshJob = this.refreshJob;
+            if (refreshJob == null || refreshJob.isCancelled() || refreshJob.isDone()) {
+                logger.debug("schedule refresh of config and status");
+                this.refreshJob = scheduler.schedule(this::doRefreshConfigAndStatus, REFRESH_CONFIG_MSEC,
+                        TimeUnit.MILLISECONDS);
+            }
+        } else {
+            logger.warn("Bridge is not online - ignoring refresh of config and status.");
+        }
+    }
+
+    private void doRefreshConfigAndStatus() {
+        final @Nullable PilightConnector connector = this.connector;
+        if (connector != null) {
+            // the config is required for dimmers to get the minimum and maximum dim levels
+            connector.refreshConfig();
+            connector.refreshStatus();
+        }
+    }
+
+    /**
+     * Processes a status update received from pilight
+     *
+     * @param status The new Status
+     */
+    private void processStatus(Status status) {
+        final Integer type = status.getType();
+        logger.trace("processStatus device '{}' type {}", status.getDevices().get(0), type);
+
+        if (!DeviceType.SERVER.equals(type)) {
+            for (Thing thing : getThing().getThings()) {
+                final @Nullable ThingHandler handler = thing.getHandler();
+                if (handler instanceof PilightBaseHandler) {
+                    ((PilightBaseHandler) handler).updateFromStatusIfMatches(status);
+                }
+            }
+        }
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(PilightDeviceDiscoveryService.class);
+    }
+
+    /**
+     * Register discovery service to this bridge instance.
+     */
+    public boolean registerDiscoveryListener(PilightDeviceDiscoveryService listener) {
+        if (discoveryService == null) {
+            discoveryService = listener;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Unregister discovery service from this bridge instance.
+     */
+    public boolean unregisterDiscoveryListener() {
+        if (discoveryService != null) {
+            discoveryService = null;
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Processes a config received from pilight
+     *
+     * @param config The new config
+     */
+    private void processConfig(Config config) {
+        for (Thing thing : getThing().getThings()) {
+            final @Nullable ThingHandler handler = thing.getHandler();
+            if (handler instanceof PilightBaseHandler) {
+                ((PilightBaseHandler) handler).updateFromConfigIfMatches(config);
+            }
+        }
+
+        final @Nullable PilightDeviceDiscoveryService discoveryService = this.discoveryService;
+        if (discoveryService != null) {
+            discoveryService.setConfig(config);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightContactHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightContactHandler.java
new file mode 100644 (file)
index 0000000..d4d26e6
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * 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.handler;
+
+import static org.openhab.binding.pilight.internal.PilightBindingConstants.CHANNEL_STATE;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.dto.Action;
+import org.openhab.binding.pilight.internal.dto.Device;
+import org.openhab.binding.pilight.internal.dto.Status;
+import org.openhab.binding.pilight.internal.types.PilightContactType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PilightContactHandler} is responsible for handling a pilight contact.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightContactHandler extends PilightBaseHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(PilightContactHandler.class);
+
+    public PilightContactHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateFromStatus(Status status) {
+        String state = status.getValues().get("state");
+        if (state != null) {
+            updateState(CHANNEL_STATE, PilightContactType.valueOf(state.toUpperCase()).toOpenClosedType());
+        }
+    }
+
+    @Override
+    void updateFromConfigDevice(Device device) {
+    }
+
+    @Override
+    protected @Nullable Action createUpdateCommand(ChannelUID channelUID, Command command) {
+        logger.warn("A contact is a read only device");
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightDimmerHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightDimmerHandler.java
new file mode 100644 (file)
index 0000000..a0d18d9
--- /dev/null
@@ -0,0 +1,126 @@
+/**
+ * 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.handler;
+
+import static org.openhab.binding.pilight.internal.PilightBindingConstants.CHANNEL_DIMLEVEL;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.dto.Action;
+import org.openhab.binding.pilight.internal.dto.Code;
+import org.openhab.binding.pilight.internal.dto.Device;
+import org.openhab.binding.pilight.internal.dto.Status;
+import org.openhab.binding.pilight.internal.dto.Values;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PilightDimmerHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightDimmerHandler extends PilightBaseHandler {
+
+    private static final int MAX_DIM_LEVEL_DEFAULT = 15;
+    private static final BigDecimal BIG_DECIMAL_100 = new BigDecimal(100);
+
+    private final Logger logger = LoggerFactory.getLogger(PilightDimmerHandler.class);
+
+    private int maxDimLevel = MAX_DIM_LEVEL_DEFAULT;
+
+    public PilightDimmerHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateFromStatus(Status status) {
+        BigDecimal dimLevel = BigDecimal.ZERO;
+        String dimLevelAsString = status.getValues().get("dimlevel");
+
+        if (dimLevelAsString != null) {
+            dimLevel = getPercentageFromDimLevel(dimLevelAsString);
+        } else {
+            // Dimmer items can can also be switched on or off in pilight.
+            // When this happens, the dimmer value is not reported. At least we know it's on or off.
+            String stateAsString = status.getValues().get("state");
+            if (stateAsString != null) {
+                State state = OnOffType.valueOf(stateAsString.toUpperCase());
+                dimLevel = state.equals(OnOffType.ON) ? BIG_DECIMAL_100 : BigDecimal.ZERO;
+            }
+        }
+
+        State state = new PercentType(dimLevel);
+        updateState(CHANNEL_DIMLEVEL, state);
+    }
+
+    @Override
+    protected void updateFromConfigDevice(Device device) {
+        Integer max = device.getDimlevelMaximum();
+
+        if (max != null) {
+            maxDimLevel = max;
+        }
+    }
+
+    @Override
+    protected @Nullable Action createUpdateCommand(ChannelUID unused, Command command) {
+        Code code = new Code();
+        code.setDevice(getName());
+
+        if (command instanceof OnOffType) {
+            code.setState(command.equals(OnOffType.ON) ? Code.STATE_ON : Code.STATE_OFF);
+        } else if (command instanceof PercentType) {
+            setDimmerValue((PercentType) command, code);
+        } else {
+            logger.warn("Only OnOffType and PercentType are supported by a dimmer.");
+            return null;
+        }
+
+        Action action = new Action(Action.ACTION_CONTROL);
+        action.setCode(code);
+        return action;
+    }
+
+    private BigDecimal getPercentageFromDimLevel(String string) {
+        return new BigDecimal(string).setScale(2).divide(new BigDecimal(maxDimLevel), RoundingMode.HALF_UP)
+                .multiply(BIG_DECIMAL_100);
+    }
+
+    private void setDimmerValue(PercentType percent, Code code) {
+        if (PercentType.ZERO.equals(percent)) {
+            // pilight is not responding to commands that set both the dimlevel to 0 and state to off.
+            // So, we're only updating the state for now
+            code.setState(Code.STATE_OFF);
+        } else {
+            BigDecimal dimlevel = percent.toBigDecimal().setScale(2).divide(BIG_DECIMAL_100, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(maxDimLevel)).setScale(0, RoundingMode.HALF_UP);
+
+            Values values = new Values();
+            values.setDimlevel(dimlevel.intValue());
+            code.setValues(values);
+            code.setState(dimlevel.compareTo(BigDecimal.ZERO) == 1 ? Code.STATE_ON : Code.STATE_OFF);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightGenericHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightGenericHandler.java
new file mode 100644 (file)
index 0000000..21adaa3
--- /dev/null
@@ -0,0 +1,138 @@
+/**
+ * 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.handler;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.PilightChannelConfiguration;
+import org.openhab.binding.pilight.internal.dto.Action;
+import org.openhab.binding.pilight.internal.dto.Device;
+import org.openhab.binding.pilight.internal.dto.Status;
+import org.openhab.core.library.CoreItemFactory;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.type.ChannelType;
+import org.openhab.core.thing.type.ChannelTypeRegistry;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.StateDescription;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PilightGenericHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightGenericHandler extends PilightBaseHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(PilightGenericHandler.class);
+
+    private final ChannelTypeRegistry channelTypeRegistry;
+
+    private final Map<ChannelUID, Boolean> channelReadOnlyMap = new HashMap<>();
+
+    public PilightGenericHandler(Thing thing, ChannelTypeRegistry channelTypeRegistry) {
+        super(thing);
+        this.channelTypeRegistry = channelTypeRegistry;
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        initializeReadOnlyChannels();
+    }
+
+    @Override
+    protected void updateFromStatus(Status status) {
+        for (Channel channel : thing.getChannels()) {
+            PilightChannelConfiguration config = channel.getConfiguration().as(PilightChannelConfiguration.class);
+            updateState(channel.getUID(),
+                    getDynamicChannelState(channel, status.getValues().get(config.getProperty())));
+        }
+    }
+
+    @Override
+    void updateFromConfigDevice(Device device) {
+    }
+
+    @Override
+    protected @Nullable Action createUpdateCommand(ChannelUID channelUID, Command command) {
+        if (isChannelReadOnly(channelUID)) {
+            logger.debug("Can't apply command '{}' to '{}' because channel is readonly.", command, channelUID.getId());
+            return null;
+        }
+
+        logger.debug("Create update command for '{}' not implemented.", channelUID.getId());
+
+        return null;
+    }
+
+    private State getDynamicChannelState(final Channel channel, final @Nullable String value) {
+        final @Nullable String acceptedItemType = channel.getAcceptedItemType();
+
+        if (value == null || acceptedItemType == null) {
+            return UnDefType.UNDEF;
+        }
+
+        switch (acceptedItemType) {
+            case CoreItemFactory.NUMBER:
+                return new DecimalType(value);
+            case CoreItemFactory.STRING:
+                return StringType.valueOf(value);
+            case CoreItemFactory.SWITCH:
+                return OnOffType.from(value);
+            default:
+                logger.trace("Type '{}' for channel '{}' not implemented", channel.getAcceptedItemType(), channel);
+                return UnDefType.UNDEF;
+        }
+    }
+
+    private void initializeReadOnlyChannels() {
+        channelReadOnlyMap.clear();
+        for (Channel channel : thing.getChannels()) {
+            final @Nullable ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
+            if (channelTypeUID != null) {
+                final @Nullable ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID, null);
+
+                if (channelType != null) {
+                    logger.debug("initializeReadOnly {} {}", channelType, channelType.getState());
+                }
+
+                if (channelType != null) {
+                    final @Nullable StateDescription state = channelType.getState();
+                    if (state != null) {
+                        channelReadOnlyMap.putIfAbsent(channel.getUID(), state.isReadOnly());
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean isChannelReadOnly(ChannelUID channelUID) {
+        Boolean isReadOnly = channelReadOnlyMap.get(channelUID);
+        return isReadOnly != null ? isReadOnly : true;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightSwitchHandler.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/handler/PilightSwitchHandler.java
new file mode 100644 (file)
index 0000000..179d6d2
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * 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.handler;
+
+import static org.openhab.binding.pilight.internal.PilightBindingConstants.CHANNEL_STATE;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pilight.internal.dto.Action;
+import org.openhab.binding.pilight.internal.dto.Code;
+import org.openhab.binding.pilight.internal.dto.Device;
+import org.openhab.binding.pilight.internal.dto.Status;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PilightSwitchHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Stefan Röllin - Initial contribution
+ * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
+ */
+@NonNullByDefault
+public class PilightSwitchHandler extends PilightBaseHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(PilightSwitchHandler.class);
+
+    public PilightSwitchHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void updateFromStatus(Status status) {
+        String state = status.getValues().get("state");
+        if (state != null) {
+            updateState(CHANNEL_STATE, OnOffType.valueOf(state.toUpperCase()));
+        }
+    }
+
+    @Override
+    void updateFromConfigDevice(Device device) {
+    }
+
+    @Override
+    protected @Nullable Action createUpdateCommand(ChannelUID unused, Command command) {
+        if (command instanceof OnOffType) {
+            Code code = new Code();
+            code.setDevice(getName());
+            code.setState(command.equals(OnOffType.ON) ? Code.STATE_ON : Code.STATE_OFF);
+
+            Action action = new Action(Action.ACTION_CONTROL);
+            action.setCode(code);
+            return action;
+        }
+
+        logger.warn("A pilight switch only accepts OnOffType commands.");
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/serializers/BooleanToIntegerSerializer.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/serializers/BooleanToIntegerSerializer.java
new file mode 100644 (file)
index 0000000..0a28a9d
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * 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.serializers;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+/**
+ * Serializer to map boolean values to an integer (1 and 0).
+ *
+ * @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 BooleanToIntegerSerializer extends JsonSerializer<Boolean> {
+
+    @Override
+    public void serialize(@Nullable Boolean bool, @Nullable JsonGenerator jsonGenerator,
+            @Nullable SerializerProvider serializerProvider) throws IOException {
+        if (bool != null && jsonGenerator != null) {
+            jsonGenerator.writeObject(bool ? 1 : 0);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/types/PilightContactType.java b/bundles/org.openhab.binding.pilight/src/main/java/org/openhab/binding/pilight/internal/types/PilightContactType.java
new file mode 100644 (file)
index 0000000..8867d83
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * 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.types;
+
+import org.openhab.core.library.types.OpenClosedType;
+
+/**
+ * Enum to represent the state of a contact sensor 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 enum PilightContactType {
+    OPENED,
+    CLOSED;
+
+    public OpenClosedType toOpenClosedType() {
+        return this.equals(PilightContactType.OPENED) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
+    }
+}
diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..2540db4
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="pilight" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Pilight Binding</name>
+       <description>The pilight binding allows openHAB to communicate with a pilight instance. Pilight is a service used to
+               control 'Click On Click Off' devices like 433 MHz remote controlled sockets a cheap way, e.g. by using a Raspberry Pi
+               with corresponding 433 MHz sender.</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..84809bc
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
+       https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:pilight:device">
+               <parameter name="name" type="text" required="true">
+                       <label>Name of Device</label>
+                       <description>The name of the pilight device.</description>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/i18n/pilight_de.properties b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/i18n/pilight_de.properties
new file mode 100644 (file)
index 0000000..b2b8e8e
--- /dev/null
@@ -0,0 +1,38 @@
+# binding
+binding.pilight.name = Pilight Binding
+binding.pilight.description = Das pilight-Binding ermöglicht openHAB die Kommunikation mit einer pilight-Instanz. Pilight ist ein Dienst, der verwendet wird, um 'Click On Click Off'-Geräte wie bspw. 433 MHz Funksteckdosen auf kostengünstige Weise zu steuern, z.B. durch Verwendung eines Raspberry Pi mit entsprechendem 433 MHz Sender.
+
+# thing types
+thing-type.pilight.bridge.label = Pilight Bridge
+thing-type.pilight.bridge.description = Verbindung zwischen openHAB und einem pilight Daemon.
+
+thing-type.pilight.contact.label = Pilight Kontakt
+thing-type.pilight.contact.description = Pilight Kontakt
+
+thing-type.pilight.dimmer.label = Pilight Dimmer
+thing-type.pilight.dimmer.description = Pilight Dimmer
+
+thing-type.pilight.switch.label = Pilight Schalter
+thing-type.pilight.switch.description = Pilight Schalter
+
+thing-type.pilight.generic.label = Generisches pilight Gerät
+thing-type.pilight.generic.description = Gerät bei dem die Kanäle dynamisch hinzugefügt werden.
+
+# thing type config description
+thing-type.config.pilight.bridge.ipAddress.label = IP-Adresse
+thing-type.config.pilight.bridge.ipAddress.description = Lokale IP-Adresse oder Hostname des pilight Daemons.
+thing-type.config.pilight.bridge.port.label = Port
+thing-type.config.pilight.bridge.port.description = Port des pilight Daemons.
+thing-type.config.pilight.bridge.delay.label = Verzögerung
+thing-type.config.pilight.bridge.delay.description = Verzögerung (in Millisekunden) zwischen zwei Kommandos. Empfohlener Wert ohne Bandpassfilter: 1000 und mit Bandpassfilter zwischen 200 und 500.
+
+thing-type.config.pilight.device.name.label = Name
+thing-type.config.pilight.device.name.description = Name des pilight Geräts
+
+# channel types
+channel-type.pilight.contact-state.label = Status
+channel-type.pilight.contact-state.description = Status des pilight Kontakts
+channel-type.pilight.switch-state.label = Status
+channel-type.pilight.switch-state.description = Status des pilight Schalters
+channel-type.pilight.dimlevel.label = Dimmerwert
+channel-type.pilight.dimlevel.description = Wert des pilight Dimmers
diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/bridge.xml
new file mode 100644 (file)
index 0000000..e52c70a
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="pilight"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="bridge">
+               <label>Pilight Bridge</label>
+               <description>Pilight Bridge which connects to a Pilight instance.</description>
+
+               <properties>
+                       <property name="firmwareVersion">-</property>
+               </properties>
+
+               <config-description>
+                       <parameter name="ipAddress" type="text" required="true">
+                               <label>Network Address</label>
+                               <description>The IP or host name of the Pilight instance.</description>
+                               <context>network-address</context>
+                       </parameter>
+                       <parameter name="port" type="integer" required="true" min="1" max="65335">
+                               <label>Port</label>
+                               <description>Port of the Pilight daemon. 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.
+                               </description>
+                               <default>5000</default>
+                       </parameter>
+                       <parameter name="delay" type="integer" required="false" min="1" max="65335">
+                               <label>Delay between Commands</label>
+                               <description>Delay (in millisecond) between consecutive commands. Recommended value without band pass filter: 1000.
+                                       Recommended value with band pass filter: somewhere between 200-500.</description>
+                               <default>500</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/devices.xml b/bundles/org.openhab.binding.pilight/src/main/resources/OH-INF/thing/devices.xml
new file mode 100644 (file)
index 0000000..c55db40
--- /dev/null
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="pilight"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="switch">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>Pilight Switch</label>
+               <description>Pilight Switch</description>
+
+               <channels>
+                       <channel id="state" typeId="system.power"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:pilight:device"/>
+       </thing-type>
+
+       <thing-type id="contact">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>Pilight Contact</label>
+               <description>Pilight Contact</description>
+
+               <channels>
+                       <channel id="state" typeId="contact-state"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:pilight:device"/>
+       </thing-type>
+
+       <thing-type id="dimmer">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>Pilight Dimmer</label>
+               <description>Pilight Dimmer</description>
+
+               <channels>
+                       <channel id="dimlevel" typeId="system.brightness"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:pilight:device"/>
+       </thing-type>
+
+       <thing-type id="generic" extensible="string,number">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>Pilight Generic Device</label>
+               <description>Pilight Generic Device</description>
+
+               <config-description-ref uri="thing-type:pilight:device"/>
+       </thing-type>
+
+       <channel-type id="contact-state">
+               <item-type>Contact</item-type>
+               <label>State of Contact</label>
+               <description>State of Pilight Contact.</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="string">
+               <item-type>String</item-type>
+               <label>Text Value</label>
+               <state readOnly="true"/>
+               <config-description>
+                       <parameter name="property" type="text">
+                               <label>Property</label>
+                               <description>The Property of the Device.</description>
+                               <required>true</required>
+                       </parameter>
+               </config-description>
+       </channel-type>
+
+       <channel-type id="number">
+               <item-type>Number</item-type>
+               <label>Number Value</label>
+               <state readOnly="true"/>
+               <config-description>
+                       <parameter name="property" type="text">
+                               <label>Property</label>
+                               <description>The Property of the Device.</description>
+                               <required>true</required>
+                       </parameter>
+               </config-description>
+       </channel-type>
+
+</thing:thing-descriptions>
index 7132c692ac7e002b6876c1110d2e55440d76c6e6..909b8d78d80eba2dba7de1a9c93f9ca5caf009ab 100644 (file)
     <module>org.openhab.binding.paradoxalarm</module>
     <module>org.openhab.binding.pentair</module>
     <module>org.openhab.binding.phc</module>
+    <module>org.openhab.binding.pilight</module>
     <module>org.openhab.binding.pioneeravr</module>
     <module>org.openhab.binding.pixometer</module>
     <module>org.openhab.binding.pjlinkdevice</module>