diff --git a/CODEOWNERS b/CODEOWNERS
index 331807f714838..41b26932599a9 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -50,6 +50,7 @@
/bundles/org.openhab.binding.dwdunwetter/ @limdul79
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
/bundles/org.openhab.binding.energenie/ @hmerk
+/bundles/org.openhab.binding.enigma2/ @gdolfen
/bundles/org.openhab.binding.enocean/ @fruggy83
/bundles/org.openhab.binding.enturno/ @klocsson
/bundles/org.openhab.binding.etherrain/ @dfad1469
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 3c6a353b6eb07..b120947b42edf 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -241,6 +241,11 @@
org.openhab.binding.energenie
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.enigma2
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.enocean
diff --git a/bundles/org.openhab.binding.enigma2/NOTICE b/bundles/org.openhab.binding.enigma2/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.enigma2/README.md b/bundles/org.openhab.binding.enigma2/README.md
new file mode 100644
index 0000000000000..fb8f2219bbc97
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/README.md
@@ -0,0 +1,409 @@
+# Enigma2 Binding
+
+The binding integrates Enigma2 devices.
+
+## Supported Things
+
+### Enigma2 devices
+
+Enigma2 based set-top boxes with an installed OpenWebIf are supported.
+
+#### Device Settings
+
+The Device must be connected to the same network as openHAB.
+
+## Discovery
+
+Devices are auto discovered through HTTP in the local network.
+
+If automatic discovery is not possible you may still manually configure a device based on the hostname.
+
+## Thing Configuration
+
+Enigma2 has the following configuration parameters:
+
+| Name | Description | Mandatory |
+|-----------------|----------------------------------------------------|-----------|
+| host | Hostname or IP address of the Enigma2 device | yes |
+| refreshInterval | The refresh interval in seconds | yes |
+| timeout | The timeout for reading from the device in seconds | yes |
+| user | Optional: The Username of the Enigma2 Web API | no |
+| password | Optional: The Password of the Enigma2 Web API | no |
+
+### Configuration in .things file
+
+Set the parameters as in the following example:
+
+```
+Thing enigma2:device:192_168_0_3 [host="192.168.1.3", refreshInterval="5", timeout="5", user="usename" , password="***"]
+```
+
+## Channels
+
+| Channel Type ID | Item Type | Description | Read/Write |
+|-----------------|-----------|----------------------------------------------------------------------------------------------|------------|
+| power | Switch | Current power setting. | RW |
+| mute | Switch | Current mute setting. | RW |
+| volume | Dimmer | Current volume setting. | RW |
+| channel | String | Current channel. Use only the channel text as command to update the channel. | RW |
+| title | String | Current program title of the current channel. | R |
+| description | String | Current program description of the current channel. | R |
+| mediaPlayer | Player | Media control player. | RW |
+| mediaStop | Switch | Media control stop. | RW |
+| answer | String | Receives an answer to a send question of the device. | R |
+
+## Example
+
+demo.things:
+
+```
+Thing enigma2:device:192_168_0_3 [host="192.168.1.3", refreshInterval="5"]
+```
+
+demo.items:
+
+```
+Switch Enigma2_Power "Power: [%s]" { channel="enigma2:device:192_168_0_3:power" }
+Dimmer Enigma2_Volume "Volume: [%d %%]" { channel="enigma2:device:192_168_0_3:volume" }
+Switch Enigma2_Mute "Mute: [%s]" { channel="enigma2:device:192_168_0_3:mute" }
+Switch Enigma2_Stop "Stop: [%s]" { channel="enigma2:device:192_168_0_3:mediaStop", autoupdate="false" }
+Player Enigma2_PlayerControl "Mode: [%s]" { channel="enigma2:device:192_168_0_3:mediaPlayer" }
+String Enigma2_Channel "Channel: [%s]" { channel="enigma2:device:192_168_0_3:channel" }
+String Enigma2_Title "Title: [%s]" { channel="enigma2:device:192_168_0_3:title" }
+String Enigma2_Description "Description: [%s]" { channel="enigma2:device:192_168_0_3:description" }
+String Enigma2_Answer "Answer: [%s]" { channel="enigma2:device:192_168_0_3:answer" }
+String Enigma2_RemoteKeys "[]" { autoupdate="false" }
+String Enigma2_SendError "Error" { autoupdate="false" }
+String Enigma2_SendWarning "Warning" { autoupdate="false" }
+String Enigma2_SendInfo "Info" { autoupdate="false" }
+```
+
+demo.sitemap:
+
+```
+sitemap demo label="Enigma2 Demo"
+{
+ Frame label="Enigma2" {
+ Switch item=Enigma2_Power
+ Slider item=Enigma2_Volume step=5 minValue=0 maxValue=100
+ Setpoint item=Enigma2_Volume step=5 minValue=0 maxValue=100
+ Switch item=Enigma2_Mute
+ Default item=Enigma2_PlayerControl
+ Switch item=Enigma2_Stop mappings=[ON="Stop"]
+ Text item=Enigma2_Channel
+ Text item=Enigma2_Title
+ Text item=Enigma2_Description
+ }
+ Frame label="Enigma2 Remote" {
+ Switch item=Enigma2_RemoteKeys mappings=[POWER="POWER"]
+ Switch item=Enigma2_RemoteKeys mappings=[TEXT="[=]", SUBTITLE="[_]", MUTE="MUTE"]
+ Switch item=Enigma2_RemoteKeys mappings=[KEY_1="1", KEY_2="2", KEY_3="3"]
+ Switch item=Enigma2_RemoteKeys mappings=[KEY_4="4", KEY_5="5", KEY_6="6"]
+ Switch item=Enigma2_RemoteKeys mappings=[KEY_7="7", KEY_8="8", KEY_9="9"]
+ Switch item=Enigma2_RemoteKeys mappings=[ARROW_LEFT="<", KEY_0="0", ARROW_RIGHT=">"]
+ Switch item=Enigma2_RemoteKeys mappings=[RED="R", GREEN="G", YELLOW="Y", BLUE="B"]
+ Switch item=Enigma2_RemoteKeys mappings=[UP="Up"]
+ Switch item=Enigma2_RemoteKeys mappings=[LEFT="Left", OK="Ok", RIGHT="Right"]
+ Switch item=Enigma2_RemoteKeys mappings=[DOWN="Down"]
+ Switch item=Enigma2_RemoteKeys mappings=[VOLUME_UP="+", EXIT="Exit", CHANNEL_UP="+"]
+ Switch item=Enigma2_RemoteKeys mappings=[VOLUME_DOWN="-", EPG="Epg", CHANNEL_DOWN="-"]
+ Switch item=Enigma2_RemoteKeys mappings=[MENU="Menu", VIDEO="[=R]", AUDIO="Audio", HELP="Help"]
+ Switch item=Enigma2_RemoteKeys mappings=[FAST_BACKWARD="<<", PLAY=">", PAUSE="||", FAST_FORWARD=">>"]
+ Switch item=Enigma2_RemoteKeys mappings=[TV="TV", RECORD="O", STOP="[]", RADIO="Radio"]
+ Switch item=Enigma2_RemoteKeys mappings=[INFO="INFO"]
+ }
+ Frame label="Enigma2 Messages" {
+ Switch item=Enigma2_SendError mappings=[SEND="SEND"]
+ Switch item=Enigma2_SendWarning mappings=[SEND="SEND"]
+ Switch item=Enigma2_SendInfo mappings=[SEND="SEND"]
+ Switch item=Enigma2_SendQuestion mappings=[SEND="SEND"]
+ Text item=Enigma2_Answer
+ }
+}
+```
+
+
+demo.rules:
+
+```
+rule "Enigma2_KeyS"
+when Item Enigma2_RemoteKeys received command
+then
+ val actions = getActions("enigma2","enigma2:device:192_168_0_3")
+ if(null === actions) {
+ logInfo("actions", "Actions not found, check thing ID")
+ return
+ }
+ actions.sendRcCommand(receivedCommand.toString)
+end
+
+rule "Enigma2_SendError"
+when Item Enigma2_SendError received command
+then
+ val actions = getActions("enigma2","enigma2:device:192_168_0_3")
+ if(null === actions) {
+ logInfo("actions", "Actions not found, check thing ID")
+ return
+ }
+ actions.sendError(receivedCommand.toString, 10)
+end
+
+rule "Enigma2_SendWarning"
+when Item Enigma2_SendWarning received command
+then
+ val actions = getActions("enigma2","enigma2:device:192_168_0_3")
+ if(null === actions) {
+ logInfo("actions", "Actions not found, check thing ID")
+ return
+ }
+ actions.sendWarning(receivedCommand.toString, 10)
+end
+
+rule "Enigma2_SendInfo"
+when Item Enigma2_SendInfo received command
+then
+ val actions = getActions("enigma2","enigma2:device:192_168_0_3")
+ if(null === actions) {
+ logInfo("actions", "Actions not found, check thing ID")
+ return
+ }
+ actions.sendInfo(receivedCommand.toString, 10)
+end
+
+rule "Enigma2_SendQuestion"
+when Item Enigma2_SendQuestion received command
+then
+ val actions = getActions("enigma2","enigma2:device:192_168_0_3")
+ if(null === actions) {
+ logInfo("actions", "Actions not found, check thing ID")
+ return
+ }
+ actions.sendQuestion(receivedCommand.toString, 10)
+end
+
+rule "Enigma2_Answer"
+when Item Enigma2_Answer received update
+then
+ val actions = getActions("enigma2","enigma2:device:192_168_0_3")
+ if(null === actions) {
+ logInfo("actions", "Actions not found, check thing ID")
+ return
+ }
+ logInfo("actions", "Answer is " + Enigma2_Answer.state)
+end
+```
+
+## Rule Actions
+
+Multiple actions are supported by this binding. In classic rules these are accessible as shown in this example (adjust getActions with your ThingId):
+
+Example
+
+```
+ val actions = getActions("enigma2","enigma2:device:192_168_0_3")
+ if(null === actions) {
+ logInfo("actions", "Actions not found, check thing ID")
+ return
+ }
+```
+
+### sendInfo(text)
+
+Sends an info message to the device with will be shown on the TV screen for 30 seconds.
+
+Parameters:
+
+| Name | Description |
+|---------|----------------------------------------------------------------------|
+| text | The text to display |
+
+Example:
+
+```
+actions.sendInfo("Hello World")
+```
+
+### sendInfo(text, timeout)
+
+Sends an info message to the device with will be shown on the TV screen.
+
+Parameters:
+
+| Name | Description |
+|---------|----------------------------------------------------------------------|
+| text | The text to display |
+| timeout | The timeout in seconds |
+
+Example:
+
+```
+actions.sendInfo("Hello World", 10)
+```
+
+### sendWarning(text)
+
+Sends a warning message to the device with will be shown on the TV screen for 30 seconds.
+
+Parameters:
+
+| Name | Description |
+|---------|----------------------------------------------------------------------|
+| text | The text to display |
+
+Example:
+
+```
+actions.sendWarning("Hello World")
+```
+
+### sendWarning(text, timeout)
+
+Sends a warning message to the device with will be shown on the TV screen.
+
+Parameters:
+
+| Name | Description |
+|---------|----------------------------------------------------------------------|
+| text | The text to display |
+| timeout | The timeout in seconds |
+
+Example:
+
+```
+actions.sendWarning("Hello World", 10)
+```
+
+### sendError(text)
+
+Sends an error message to the device with will be shown on the TV screen for 30 seconds.
+
+Parameters:
+
+| Name | Description |
+|---------|----------------------------------------------------------------------|
+| text | The text to display |
+
+Example:
+
+```
+actions.sendError("Hello World")
+```
+
+### sendError(text, timeout)
+
+Sends an error message to the device with will be shown on the TV screen.
+
+Parameters:
+
+| Name | Description |
+|---------|----------------------------------------------------------------------|
+| text | The text to display |
+| timeout | The timeout in seconds |
+
+Example:
+
+```
+actions.sendError("Hello World", 10)
+```
+
+### sendQuestion(text)
+
+Sends a question message to the device with will be shown on the TV screen for 30 seconds.
+The answer is provided to the "answer"-channel.
+
+Parameters:
+
+| Name | Description |
+|---------|----------------------------------------------------------------------|
+| text | The text to display |
+
+Example:
+
+```
+actions.sendQuestion("Say hello?")
+```
+
+### sendQuestion(text, timeout)
+
+Sends an question message to the device with will be shown on the TV screen.
+The answer is provided to the "answer"-channel.
+
+Parameters:
+
+| Name | Description |
+|---------|----------------------------------------------------------------------|
+| text | The text to display |
+| timeout | The timeout in seconds |
+
+Example:
+
+```
+actions.sendQuestion("Say hello?", 10)
+```
+
+### sendRcCommand(button)
+
+Sends a button press event to the device.
+
+Parameters:
+
+| Name | Description |
+|---------|------------------------------------------------------------------------|
+| button | see the supported buttons in chapter 'Remote Control Buttons' |
+
+
+The button parameter has only been tested on a Vu+Solo2 and this is a list of button codes that are known to work with this device.
+
+| Code String |
+|---------------|
+| POWER |
+| KEY_0 |
+| KEY_1 |
+| KEY_2 |
+| KEY_3 |
+| KEY_4 |
+| KEY_5 |
+| KEY_6 |
+| KEY_7 |
+| KEY_8 |
+| KEY_9 |
+| ARROW_LEFT |
+| ARROW_RIGHT |
+| VOLUME_DOWN |
+| VOLUME_UP |
+| MUTE |
+| CHANNEL_UP |
+| CHANNEL_DOWN |
+| LEFT |
+| RIGHT |
+| UP |
+| DOWN |
+| OK |
+| EXIT |
+| RED |
+| GREEN |
+| YELLOW |
+| BLUE |
+| PLAY |
+| PAUSE |
+| STOP |
+| RECORD |
+| FAST_FORWARD |
+| FAST_BACKWARD |
+| TV |
+| RADIO |
+| AUDIO |
+| VIDEO |
+| TEXT |
+| INFO |
+| MENU |
+| HELP |
+| SUBTITLE |
+| EPG |
+
+Example:
+
+```
+actions.sendRcCommand("KEY_1")
+```
+
diff --git a/bundles/org.openhab.binding.enigma2/pom.xml b/bundles/org.openhab.binding.enigma2/pom.xml
new file mode 100644
index 0000000000000..82f7c91880db7
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/pom.xml
@@ -0,0 +1,16 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 2.5.6-SNAPSHOT
+
+
+ org.openhab.binding.enigma2
+
+ openHAB Add-ons :: Bundles :: Enigma2 Binding
+
+
diff --git a/bundles/org.openhab.binding.enigma2/src/main/feature/feature.xml b/bundles/org.openhab.binding.enigma2/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..c11cd51078003
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.enigma2/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/actions/Enigma2Actions.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/actions/Enigma2Actions.java
new file mode 100644
index 0000000000000..f49b54357887e
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/actions/Enigma2Actions.java
@@ -0,0 +1,180 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.actions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.binding.ThingActions;
+import org.eclipse.smarthome.core.thing.binding.ThingActionsScope;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.openhab.binding.enigma2.handler.Enigma2Handler;
+import org.openhab.binding.enigma2.internal.Enigma2BindingConstants;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.RuleAction;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * This is the automation engine actions handler service for the
+ * enigma2 actions.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@ThingActionsScope(name = "enigma2")
+@NonNullByDefault
+public class Enigma2Actions implements ThingActions, IEnigma2Actions {
+ private @Nullable Enigma2Handler handler;
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ this.handler = (Enigma2Handler) handler;
+ }
+
+ @Override
+ public @Nullable Enigma2Handler getThingHandler() {
+ return this.handler;
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-rc-button.label", description = "@text/actions.enigma2.send-rc-button.description")
+ @SuppressWarnings("null")
+ public void sendRcCommand(
+ @ActionInput(name = "rcButton", label = "@text/actions-input.enigma2.rc-button.label", description = "@text/actions-input.enigma2.rc-button.description") String rcButton) {
+ handler.sendRcCommand(rcButton);
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-info.label", description = "@text/actions.enigma2.send-info.description")
+ @SuppressWarnings("null")
+ public void sendInfo(
+ @ActionInput(name = "text", label = "@text/actions-input.enigma2.text.label", description = "@text/actions-input.enigma2.text.description") String text) {
+ handler.sendInfo(Enigma2BindingConstants.MESSAGE_TIMEOUT, text);
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-info.label", description = "@text/actions.enigma2.send-info.description")
+ @SuppressWarnings("null")
+ public void sendInfo(
+ @ActionInput(name = "text", label = "@text/actions-input.enigma2.text.label", description = "@text/actions-input.enigma2.text.description") String text,
+ @ActionInput(name = "timeout", label = "@text/actions-input.enigma2.timeout.label", description = "@text/actions-input.enigma2.timeout.description") int timeout) {
+ handler.sendInfo(timeout, text);
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-warning.label", description = "@text/actions.enigma2.send-warning.description")
+ @SuppressWarnings("null")
+ public void sendWarning(
+ @ActionInput(name = "text", label = "@text/actions-input.enigma2.text.label", description = "@text/actions-input.enigma2.text.description") String text) {
+ handler.sendWarning(Enigma2BindingConstants.MESSAGE_TIMEOUT, text);
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-warning.label", description = "@text/actions.enigma2.send-warning.description")
+ @SuppressWarnings("null")
+ public void sendWarning(
+ @ActionInput(name = "text", label = "@text/actions-input.enigma2.text.label", description = "@text/actions-input.enigma2.text.description") String text,
+ @ActionInput(name = "timeout", label = "@text/actions-input.enigma2.timeout.label", description = "@text/actions-input.enigma2.timeout.description") int timeout) {
+ handler.sendWarning(timeout, text);
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-error.label", description = "@text/actions.enigma2.send-error.description")
+ @SuppressWarnings("null")
+ public void sendError(
+ @ActionInput(name = "text", label = "@text/actions-input.enigma2.text.label", description = "@text/actions-input.enigma2.text.description") String text) {
+ handler.sendError(Enigma2BindingConstants.MESSAGE_TIMEOUT, text);
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-error.label", description = "@text/actions.enigma2.send-error.description")
+ @SuppressWarnings("null")
+ public void sendError(
+ @ActionInput(name = "text", label = "@text/actions-input.enigma2.text.label", description = "@text/actions-input.enigma2.text.description") String text,
+ @ActionInput(name = "timeout", label = "@text/actions-input.enigma2.timeout.label", description = "@text/actions-input.enigma2.timeout.description") int timeout) {
+ handler.sendError(timeout, text);
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-error.label", description = "@text/actions.enigma2.send-question.description")
+ @SuppressWarnings("null")
+ public void sendQuestion(
+ @ActionInput(name = "text", label = "@text/actions-input.enigma2.text.label", description = "@text/actions-input.enigma2.text.description") String text) {
+ handler.sendQuestion(Enigma2BindingConstants.MESSAGE_TIMEOUT, text);
+ }
+
+ @Override
+ @RuleAction(label = "@text/actions.enigma2.send-error.label", description = "@text/actions.enigma2.send-question.description")
+ @SuppressWarnings("null")
+ public void sendQuestion(
+ @ActionInput(name = "text", label = "@text/actions-input.enigma2.text.label", description = "@text/actions-input.enigma2.text.description") String text,
+ @ActionInput(name = "timeout", label = "@text/actions-input.enigma2.timeout.label", description = "@text/actions-input.enigma2.timeout.description") int timeout) {
+ handler.sendQuestion(timeout, text);
+ }
+
+ // delegation methods for "legacy" rule support
+ public static void sendRcCommand(@Nullable ThingActions actions, String rcButton) {
+ invokeMethodOf(actions).sendRcCommand(rcButton);
+ }
+
+ public static void sendInfo(@Nullable ThingActions actions, String info) {
+ invokeMethodOf(actions).sendInfo(info);
+ }
+
+ public static void sendInfo(@Nullable ThingActions actions, String info, int timeout) {
+ invokeMethodOf(actions).sendInfo(info, timeout);
+ }
+
+ public static void sendWarning(@Nullable ThingActions actions, String warning) {
+ invokeMethodOf(actions).sendWarning(warning);
+ }
+
+ public static void sendWarning(@Nullable ThingActions actions, String warning, int timeout) {
+ invokeMethodOf(actions).sendWarning(warning, timeout);
+ }
+
+ public static void sendError(@Nullable ThingActions actions, String error) {
+ invokeMethodOf(actions).sendError(error);
+ }
+
+ public static void sendError(@Nullable ThingActions actions, String error, int timeout) {
+ invokeMethodOf(actions).sendError(error, timeout);
+ }
+
+ public static void sendQuestion(@Nullable ThingActions actions, String text) {
+ invokeMethodOf(actions).sendQuestion(text);
+ }
+
+ public static void sendQuestion(@Nullable ThingActions actions, String text, int timeout) {
+ invokeMethodOf(actions).sendQuestion(text, timeout);
+ }
+
+ private static IEnigma2Actions invokeMethodOf(@Nullable ThingActions actions) {
+ if (actions == null) {
+ throw new IllegalArgumentException("actions cannot be null");
+ }
+ if (actions.getClass().getName().equals(Enigma2Actions.class.getName())) {
+ if (actions instanceof IEnigma2Actions) {
+ return (IEnigma2Actions) actions;
+ } else {
+ return (IEnigma2Actions) Proxy.newProxyInstance(IEnigma2Actions.class.getClassLoader(),
+ new Class[] { IEnigma2Actions.class }, (Object proxy, Method method, Object[] args) -> {
+ Method m = actions.getClass().getDeclaredMethod(method.getName(),
+ method.getParameterTypes());
+ return m.invoke(actions, args);
+ });
+ }
+ }
+ throw new IllegalArgumentException("Actions is not an instance of Enigma2Actions");
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/actions/IEnigma2Actions.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/actions/IEnigma2Actions.java
new file mode 100644
index 0000000000000..591d2565cf13c
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/actions/IEnigma2Actions.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.actions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link IEnigma2Actions} defines the interface for all thing actions supported by the binding.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+public interface IEnigma2Actions {
+ void sendRcCommand(String rcButton);
+
+ void sendInfo(String text);
+
+ void sendInfo(String text, int timeout);
+
+ void sendWarning(String text);
+
+ void sendWarning(String text, int timeout);
+
+ void sendError(String text);
+
+ void sendError(String text, int timeout);
+
+ void sendQuestion(String text);
+
+ void sendQuestion(String text, int timeout);
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/handler/Enigma2Handler.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/handler/Enigma2Handler.java
new file mode 100644
index 0000000000000..37c18cfc09c1e
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/handler/Enigma2Handler.java
@@ -0,0 +1,301 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.handler;
+
+import static org.openhab.binding.enigma2.internal.Enigma2BindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.library.types.*;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerService;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.RefreshType;
+import org.openhab.binding.enigma2.actions.Enigma2Actions;
+import org.openhab.binding.enigma2.internal.Enigma2Client;
+import org.openhab.binding.enigma2.internal.Enigma2Configuration;
+import org.openhab.binding.enigma2.internal.Enigma2RemoteKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * The {@link Enigma2Handler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+public class Enigma2Handler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(Enigma2Handler.class);
+ private Enigma2Configuration configuration = new Enigma2Configuration();
+ private Optional enigma2Client = Optional.empty();
+ private @Nullable ScheduledFuture> refreshJob;
+ private LocalDateTime lastAnswerTime = LocalDateTime.now();
+
+ public Enigma2Handler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void initialize() {
+ configuration = getConfigAs(Enigma2Configuration.class);
+ if (configuration.host.isEmpty()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "host must not be empty");
+ } else if (configuration.timeout <= 0 || configuration.timeout > 300) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "timeout must be between 0 and 300 seconds");
+ } else if (configuration.refreshInterval <= 0 || configuration.refreshInterval > 3600) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "refreshInterval must be between 0 and 3600 seconds");
+ }
+ enigma2Client = Optional.of(new Enigma2Client(configuration.host, configuration.user, configuration.password,
+ configuration.timeout));
+ refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 2, configuration.refreshInterval, TimeUnit.SECONDS);
+ }
+
+ private void refresh() {
+ getEnigma2Client().ifPresent(client -> {
+ boolean online = client.refresh();
+ if (online) {
+ updateStatus(ThingStatus.ONLINE);
+ updateState(CHANNEL_POWER, client.isPower() ? OnOffType.ON : OnOffType.OFF);
+ updateState(CHANNEL_MUTE, client.isMute() ? OnOffType.ON : OnOffType.OFF);
+ updateState(CHANNEL_VOLUME, new PercentType(client.getVolume()));
+ updateState(CHANNEL_CHANNEL, new StringType(client.getChannel()));
+ updateState(CHANNEL_TITLE, new StringType(client.getTitle()));
+ updateState(CHANNEL_DESCRIPTION, new StringType(client.getDescription()));
+ if (lastAnswerTime.isBefore(client.getLastAnswerTime())) {
+ lastAnswerTime = client.getLastAnswerTime();
+ updateState(CHANNEL_ANSWER, new StringType(client.getAnswer()));
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ });
+ }
+
+ @Override
+ public void dispose() {
+ ScheduledFuture> job = this.refreshJob;
+ if(job != null) {
+ job.cancel(true);
+ }
+ this.refreshJob = null;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("handleCommand({},{})", channelUID, command);
+ getEnigma2Client().ifPresent(client -> {
+ switch (channelUID.getId()) {
+ case CHANNEL_POWER:
+ handlePower(channelUID, command, client);
+ break;
+ case CHANNEL_CHANNEL:
+ handleChannel(channelUID, command, client);
+ break;
+ case CHANNEL_MEDIA_PLAYER:
+ handleMediaPlayer(channelUID, command);
+ break;
+ case CHANNEL_MEDIA_STOP:
+ handleMediaStop(channelUID, command);
+ break;
+ case CHANNEL_MUTE:
+ handleMute(channelUID, command, client);
+ break;
+ case CHANNEL_VOLUME:
+ handleVolume(channelUID, command, client);
+ break;
+ case CHANNEL_TITLE:
+ handleTitle(channelUID, command, client);
+ break;
+ case CHANNEL_DESCRIPTION:
+ handleDescription(channelUID, command, client);
+ break;
+ case CHANNEL_ANSWER:
+ handleAnswer(channelUID, command, client);
+ break;
+ default:
+ logger.debug("Channel {} is not supported", channelUID);
+ break;
+ }
+ });
+ }
+
+ private void handleVolume(ChannelUID channelUID, Command command, Enigma2Client client) {
+ if (command instanceof RefreshType) {
+ client.refreshVolume();
+ updateState(channelUID, new PercentType(client.getVolume()));
+ } else if (command instanceof PercentType) {
+ client.setVolume(((PercentType) command).intValue());
+ } else if (command instanceof DecimalType) {
+ client.setVolume(((DecimalType) command).intValue());
+ } else {
+ logger.info("Channel {} only accepts PercentType, DecimalType, RefreshType. Type was {}.", channelUID,
+ command.getClass());
+ }
+ }
+
+ private void handleMute(ChannelUID channelUID, Command command, Enigma2Client client) {
+ if (command instanceof RefreshType) {
+ client.refreshVolume();
+ updateState(channelUID, client.isMute() ? OnOffType.ON : OnOffType.OFF);
+ } else if (OnOffType.ON.equals(command)) {
+ client.setMute(true);
+ } else if (OnOffType.OFF.equals(command)) {
+ client.setMute(false);
+ } else {
+ logger.info("Channel {} only accepts OnOffType, RefreshType. Type was {}.", channelUID, command.getClass());
+ }
+ }
+
+ private void handleAnswer(ChannelUID channelUID, Command command, Enigma2Client client) {
+ if (command instanceof RefreshType) {
+ client.refreshAnswer();
+ if (lastAnswerTime.isBefore(client.getLastAnswerTime())) {
+ lastAnswerTime = client.getLastAnswerTime();
+ updateState(channelUID, new StringType(client.getAnswer()));
+ }
+ } else {
+ logger.info("Channel {} only accepts RefreshType. Type was {}.", channelUID, command.getClass());
+ }
+ }
+
+ private void handleMediaStop(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ return;
+ } else if (command instanceof OnOffType) {
+ sendRcCommand(Enigma2RemoteKey.STOP);
+ } else {
+ logger.info("Channel {} only accepts OnOffType, RefreshType. Type was {}.", channelUID, command.getClass());
+ }
+ }
+
+ private void handleMediaPlayer(ChannelUID channelUID, Command command) {
+ if (RefreshType.REFRESH == command) {
+ return;
+ } else if (PlayPauseType.PLAY == command) {
+ sendRcCommand(Enigma2RemoteKey.PLAY);
+ } else if (PlayPauseType.PAUSE == command) {
+ sendRcCommand(Enigma2RemoteKey.PAUSE);
+ } else if (NextPreviousType.NEXT == command) {
+ sendRcCommand(Enigma2RemoteKey.FAST_FORWARD);
+ } else if (NextPreviousType.PREVIOUS == command) {
+ sendRcCommand(Enigma2RemoteKey.FAST_BACKWARD);
+ } else {
+ logger.info("Channel {} only accepts PlayPauseType, NextPreviousType, RefreshType. Type was {}.",
+ channelUID, command.getClass());
+ }
+ }
+
+ private void handleChannel(ChannelUID channelUID, Command command, Enigma2Client client) {
+ if (command instanceof RefreshType) {
+ client.refreshChannel();
+ updateState(channelUID, new StringType(client.getChannel()));
+ } else if (command instanceof StringType) {
+ client.setChannel(command.toString());
+ } else {
+ logger.info("Channel {} only accepts StringType, RefreshType. Type was {}.", channelUID,
+ command.getClass());
+ }
+ }
+
+ private void handleTitle(ChannelUID channelUID, Command command, Enigma2Client client) {
+ if (command instanceof RefreshType) {
+ client.refreshEpg();
+ updateState(channelUID, new StringType(client.getTitle()));
+ } else {
+ logger.info("Channel {} only accepts RefreshType. Type was {}.", channelUID, command.getClass());
+ }
+ }
+
+ private void handleDescription(ChannelUID channelUID, Command command, Enigma2Client client) {
+ if (command instanceof RefreshType) {
+ client.refreshEpg();
+ updateState(channelUID, new StringType(client.getDescription()));
+ } else {
+ logger.info("Channel {} only accepts RefreshType. Type was {}.", channelUID, command.getClass());
+ }
+ }
+
+ private void handlePower(ChannelUID channelUID, Command command, Enigma2Client client) {
+ if (RefreshType.REFRESH == command) {
+ client.refreshPower();
+ updateState(channelUID, client.isPower() ? OnOffType.ON : OnOffType.OFF);
+ } else if (OnOffType.ON == command) {
+ client.setPower(true);
+ } else if (OnOffType.OFF == command) {
+ client.setPower(false);
+ } else {
+ logger.info("Channel {} only accepts OnOffType, RefreshType. Type was {}.", channelUID, command.getClass());
+ }
+ }
+
+ public void sendRcCommand(String rcButton) {
+ logger.debug("sendRcCommand({})", rcButton);
+ try {
+ Enigma2RemoteKey remoteKey = Enigma2RemoteKey.valueOf(rcButton);
+ sendRcCommand(remoteKey);
+ } catch (IllegalArgumentException ex) {
+ logger.warn("{} is not a valid value for button - available are: {}", rcButton,
+ Stream.of(Enigma2RemoteKey.values()).map(b -> b.name()).collect(Collectors.joining(", ")));
+ }
+ }
+
+ private void sendRcCommand(Enigma2RemoteKey remoteKey) {
+ getEnigma2Client().ifPresent(client -> client.sendRcCommand(remoteKey.getValue()));
+ }
+
+ public void sendInfo(int timeout, String text) {
+ getEnigma2Client().ifPresent(client -> client.sendInfo(timeout, text));
+ }
+
+ public void sendWarning(int timeout, String text) {
+ getEnigma2Client().ifPresent(client -> client.sendWarning(timeout, text));
+ }
+
+ public void sendError(int timeout, String text) {
+ getEnigma2Client().ifPresent(client -> client.sendError(timeout, text));
+ }
+
+ public void sendQuestion(int timeout, String text) {
+ getEnigma2Client().ifPresent(client -> client.sendQuestion(timeout, text));
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Collections.singleton(Enigma2Actions.class);
+ }
+
+ /**
+ * Getter for Test-Injection
+ *
+ * @return Enigma2Client.
+ */
+ Optional getEnigma2Client() {
+ return enigma2Client;
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2BindingConstants.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2BindingConstants.java
new file mode 100644
index 0000000000000..57844e7a5295d
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2BindingConstants.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * The {@link Enigma2BindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+public class Enigma2BindingConstants {
+
+ private static final String BINDING_ID = "enigma2";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "device");
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_DEVICE);
+
+ // List of all Channel ids
+ public static final String CHANNEL_VOLUME = "volume";
+ public static final String CHANNEL_POWER = "power";
+ public static final String CHANNEL_MUTE = "mute";
+ public static final String CHANNEL_CHANNEL = "channel";
+ public static final String CHANNEL_TITLE = "title";
+ public static final String CHANNEL_DESCRIPTION = "description";
+ public static final String CHANNEL_MEDIA_PLAYER = "mediaPlayer";
+ public static final String CHANNEL_MEDIA_STOP = "mediaStop";
+ public static final String CHANNEL_ANSWER = "answer";
+
+ // List of all configuration parameters
+ public static final String CONFIG_HOST = "host";
+ public static final String CONFIG_USER = "user";
+ public static final String CONFIG_PASSWORD = "password";
+ public static final String CONFIG_REFRESH = "refreshInterval";
+ public static final String CONFIG_TIMEOUT = "timeout";
+
+ public static final int MESSAGE_TIMEOUT = 30;
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Client.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Client.java
new file mode 100644
index 0000000000000..8cf8da55d5321
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Client.java
@@ -0,0 +1,351 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.commons.lang.StringUtils;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.util.UrlEncoded;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * The {@link Enigma2Client} class is responsible for communicating with the Enigma2 device.
+ *
+ * @see OpenWebif-API-documentation
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+public class Enigma2Client {
+ private final Logger logger = LoggerFactory.getLogger(Enigma2Client.class);
+
+ static final String PATH_REMOTE_CONTROL = "/web/remotecontrol?command=";
+ static final String PATH_POWER = "/web/powerstate";
+ static final String PATH_VOLUME = "/web/vol";
+ static final String PATH_SET_VOLUME = "/web/vol?set=set";
+ static final String PATH_TOGGLE_MUTE = "/web/vol?set=mute";
+ static final String PATH_TOGGLE_POWER = "/web/powerstate?newstate=0";
+ static final String PATH_MESSAGE = "/web/message?type=";
+ static final String PATH_ALL_SERVICES = "/web/getallservices";
+ static final String PATH_ZAP = "/web/zap?sRef=";
+ static final String PATH_CHANNEL = "/web/subservices";
+ static final String PATH_EPG = "/web/epgservicenow?sRef=";
+ static final String PATH_ANSWER = "/web/messageanswer?getanswer=now";
+ static final int TYPE_QUESTION = 0;
+ static final int TYPE_INFO = 1;
+ static final int TYPE_WARNING = 2;
+ static final int TYPE_ERROR = 3;
+ private final Map channels = new ConcurrentHashMap<>();
+ private final String host;
+ private boolean power;
+ private String channel = "";
+ private String title = "";
+ private String description = "";
+ private String answer = "";
+ private int volume = 0;
+ private boolean mute;
+ private boolean online;
+ private boolean initialized;
+ private boolean asking;
+ private LocalDateTime lastAnswerTime = LocalDateTime.of(2020, 1, 1, 0, 0); // Date in the past
+ private final Enigma2HttpClient enigma2HttpClient;
+ private final DocumentBuilderFactory factory;
+
+ public Enigma2Client(String host, @Nullable String user, @Nullable String password, int requestTimeout) {
+ this.enigma2HttpClient = new Enigma2HttpClient(requestTimeout);
+ this.factory = DocumentBuilderFactory.newInstance();
+ if (StringUtils.isNotEmpty(user) && StringUtils.isNotEmpty(password)) {
+ this.host = "http://" + user + ":" + password + "@" + host;
+ } else {
+ this.host = "http://" + host;
+ }
+ }
+
+ public boolean refresh() {
+ boolean wasOnline = online;
+ refreshPower();
+ if (!wasOnline && online) {
+ // Only refresh all services if the box changed from offline to online and power is on
+ // because it is a performance intensive action.
+ refreshAllServices();
+ }
+ refreshChannel();
+ refreshEpg();
+ refreshVolume();
+ refreshAnswer();
+ return online;
+ }
+
+ public void refreshPower() {
+ Optional document = transmitWithResult(PATH_POWER);
+ if (document.isPresent()) {
+ online = true;
+ processPowerResult(document.get());
+ } else {
+ online = false;
+ power = false;
+ }
+ initialized = true;
+ }
+
+ public void refreshAllServices() {
+ if (power || channels.isEmpty()) {
+ transmitWithResult(PATH_ALL_SERVICES).ifPresent(this::processAllServicesResult);
+ }
+ }
+
+ public void refreshChannel() {
+ if (power) {
+ transmitWithResult(PATH_CHANNEL).ifPresent(this::processChannelResult);
+ }
+ }
+
+ public void refreshAnswer() {
+ if (asking) {
+ transmitWithResult(PATH_ANSWER).ifPresent(this::processAnswerResult);
+ }
+ }
+
+ public void refreshVolume() {
+ if (power) {
+ transmitWithResult(PATH_VOLUME).ifPresent(this::processVolumeResult);
+ }
+ }
+
+ public void refreshEpg() {
+ if (power) {
+ Optional.ofNullable(channels.get(channel))
+ .flatMap(name -> transmitWithResult(PATH_EPG + UrlEncoded.encodeString(name)))
+ .ifPresent(this::processEpgResult);
+ }
+ }
+
+ private Optional transmitWithResult(String path) {
+ try {
+ Optional xml = transmit(path);
+ if(xml.isPresent()) {
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ return Optional.ofNullable(builder.parse(new InputSource(new StringReader(xml.get()))));
+ }
+ return Optional.empty();
+ } catch (IOException | SAXException | ParserConfigurationException | IllegalArgumentException e) {
+ if (online || !initialized) {
+ logger.debug("Error on transmit {}{}.", host, path, e);
+ }
+ return Optional.empty();
+ }
+ }
+
+ private Optional transmit(String path) {
+ String url = host + path;
+ try {
+ logger.debug("Transmitting {}", url);
+ String result = getEnigma2HttpClient().get(url);
+ logger.debug("Transmitting result is {}", result);
+ return Optional.ofNullable(result);
+ } catch (IOException | IllegalArgumentException e) {
+ if (online || !initialized) {
+ logger.debug("Error on transmit {}.", url, e);
+ }
+ return Optional.empty();
+ }
+ }
+
+ public void setMute(boolean mute) {
+ refreshVolume();
+ if (this.mute != mute) {
+ transmitWithResult(PATH_TOGGLE_MUTE).ifPresent(this::processVolumeResult);
+ }
+ }
+
+ public void setPower(boolean power) {
+ refreshPower();
+ if (this.power != power) {
+ transmitWithResult(PATH_TOGGLE_POWER).ifPresent(this::processPowerResult);
+ }
+ }
+
+ public void setVolume(int volume) {
+ transmitWithResult(PATH_SET_VOLUME + volume).ifPresent(this::processVolumeResult);
+ }
+
+ public void setChannel(String name) {
+ if (channels.containsKey(name)) {
+ String id = channels.get(name);
+ transmitWithResult(PATH_ZAP + UrlEncoded.encodeString(id)).ifPresent(document -> channel = name);
+ } else {
+ logger.warn("Channel {} not found.", name);
+ }
+ }
+
+ public void sendRcCommand(int key) {
+ transmit(PATH_REMOTE_CONTROL + key);
+ }
+
+ public void sendError(int timeout, String text) {
+ sendMessage(TYPE_ERROR, timeout, text);
+ }
+
+ public void sendWarning(int timeout, String text) {
+ sendMessage(TYPE_WARNING, timeout, text);
+ }
+
+ public void sendInfo(int timeout, String text) {
+ sendMessage(TYPE_INFO, timeout, text);
+ }
+
+ public void sendQuestion(int timeout, String text) {
+ asking = true;
+ sendMessage(TYPE_QUESTION, timeout, text);
+ }
+
+ private void sendMessage(int type, int timeout, String text) {
+ transmit(PATH_MESSAGE + type + "&timeout=" + timeout + "&text=" + UrlEncoded.encodeString(text));
+ }
+
+ private void processPowerResult(Document document) {
+ power = !getBoolean(document, "e2instandby");
+ if (!power) {
+ title = "";
+ description = "";
+ channel = "";
+ }
+ }
+
+ private void processChannelResult(Document document) {
+ channel = getString(document, "e2servicename");
+ // Add channel-Reference-ID if not known
+ if (!channels.containsKey(channel)) {
+ channels.put(channel, getString(document, "e2servicereference"));
+ }
+ }
+
+ private void processAnswerResult(Document document) {
+ if (asking) {
+ boolean state = getBoolean(document, "e2state");
+ if (state) {
+ String[] text = getString(document, "e2statetext").split(" ");
+ answer = text[text.length - 1].replace("!", "");
+ asking = false;
+ lastAnswerTime = LocalDateTime.now();
+ }
+ }
+ }
+
+ private void processVolumeResult(Document document) {
+ volume = getInt(document, "e2current");
+ mute = getBoolean(document, "e2ismuted");
+ }
+
+ private void processEpgResult(Document document) {
+ title = getString(document, "e2eventtitle");
+ description = getString(document, "e2eventdescription");
+ }
+
+ private void processAllServicesResult(Document document) {
+ NodeList bouquetList = document.getElementsByTagName("e2bouquet");
+ channels.clear();
+ for (int i = 0; i < bouquetList.getLength(); i++) {
+ Element bouquet = (Element) bouquetList.item(i);
+ NodeList serviceList = bouquet.getElementsByTagName("e2service");
+ for (int j = 0; j < serviceList.getLength(); j++) {
+ Element service = (Element) serviceList.item(j);
+ String id = service.getElementsByTagName("e2servicereference").item(0).getTextContent();
+ String name = service.getElementsByTagName("e2servicename").item(0).getTextContent();
+ channels.put(name, id);
+ }
+ }
+ }
+
+ private String getString(Document document, String elementId) {
+ return Optional.ofNullable(document.getElementsByTagName(elementId)).map(nodeList -> nodeList.item(0))
+ .map(Node::getTextContent).map(String::trim).orElse("");
+ }
+
+ private boolean getBoolean(Document document, String elementId) {
+ return Boolean.parseBoolean(getString(document, elementId));
+ }
+
+ private int getInt(Document document, String elementId) {
+ try {
+ return Integer.parseInt(getString(document, elementId));
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ public int getVolume() {
+ return volume;
+ }
+
+ public boolean isMute() {
+ return mute;
+ }
+
+ public boolean isPower() {
+ return power;
+ }
+
+ public LocalDateTime getLastAnswerTime() {
+ return lastAnswerTime;
+ }
+
+ public String getChannel() {
+ return channel;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getAnswer() {
+ return answer;
+ }
+
+ public Collection getChannels() {
+ return channels.keySet();
+ }
+
+ /**
+ * Getter for Test-Injection
+ *
+ * @return HttpGet.
+ */
+ Enigma2HttpClient getEnigma2HttpClient() {
+ return enigma2HttpClient;
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Configuration.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Configuration.java
new file mode 100644
index 0000000000000..23830fd8354ad
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Configuration.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Enigma2Configuration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+public class Enigma2Configuration {
+
+ /**
+ * Hostname or IP address of the Enigma2 device.
+ */
+ public String host = "";
+ /**
+ * The refresh interval in seconds.
+ */
+ public int refreshInterval = 5;
+ /**
+ * The refresh interval in seconds.
+ */
+ public int timeout = 5;
+ /**
+ * The Username of the Enigma2 Web API.
+ */
+ public String user = "";
+ /**
+ * The Password of the Enigma2 Web API.
+ */
+ public String password = "";
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2HandlerFactory.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2HandlerFactory.java
new file mode 100644
index 0000000000000..9625a3a2377ef
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2HandlerFactory.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import static org.openhab.binding.enigma2.internal.Enigma2BindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
+import org.openhab.binding.enigma2.handler.Enigma2Handler;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link Enigma2HandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.enigma2", service = ThingHandlerFactory.class)
+public class Enigma2HandlerFactory extends BaseThingHandlerFactory {
+
+ @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_DEVICE.equals(thingTypeUID)) {
+ return new Enigma2Handler(thing);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2HttpClient.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2HttpClient.java
new file mode 100644
index 0000000000000..a0c8e70b8d4ae
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2HttpClient.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.io.net.http.HttpUtil;
+
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+/**
+ * The {@link Enigma2HttpClient} class is responsible for sending HTTP-Get requests to the Enigma2 device.
+ * It is devided from {@link Enigma2Client} for better testing purpose.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+public class Enigma2HttpClient {
+ public static final Pattern PATTERN = Pattern.compile("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD\\u10000-\\u10FFF]+");
+ private final int timeout;
+
+ public Enigma2HttpClient(int timeout) {
+ this.timeout = timeout;
+ }
+
+ public String get(String url) throws IOException, IllegalArgumentException {
+ String xml = HttpUtil.executeUrl("GET", url, timeout * 1000);
+ // remove some unsupported xml-characters
+ return PATTERN.matcher(xml).replaceAll("");
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2RemoteKey.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2RemoteKey.java
new file mode 100644
index 0000000000000..4492e11d1a445
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2RemoteKey.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Enigma2RemoteKey} class defines the remote keys of an enigma2 device
+ * used across the whole binding.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+public enum Enigma2RemoteKey {
+ POWER(116),
+
+ KEY_0(11),
+ KEY_1(2),
+ KEY_2(3),
+ KEY_3(4),
+ KEY_4(5),
+ KEY_5(6),
+ KEY_6(7),
+ KEY_7(8),
+ KEY_8(9),
+ KEY_9(10),
+
+ ARROW_LEFT(412),
+ ARROW_RIGHT(407),
+
+ VOLUME_DOWN(114),
+ VOLUME_UP(115),
+ MUTE(113),
+
+ CHANNEL_UP(402),
+ CHANNEL_DOWN(403),
+
+ LEFT(105),
+ RIGHT(106),
+ UP(103),
+ DOWN(108),
+ OK(352),
+ EXIT(174),
+
+ RED(398),
+ GREEN(399),
+ YELLOW(400),
+ BLUE(401),
+
+ PLAY(207),
+ PAUSE(119),
+ STOP(128),
+ RECORD(167),
+ FAST_FORWARD(208),
+ FAST_BACKWARD(168),
+
+ TV(377),
+ RADIO(385),
+ AUDIO(392),
+ VIDEO(393),
+ TEXT(388),
+ INFO(358),
+ MENU(139),
+ HELP(138),
+ SUBTITLE(370),
+ EPG(358);
+
+ private final int value;
+
+ Enigma2RemoteKey(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/discovery/Enigma2DiscoveryParticipant.java b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/discovery/Enigma2DiscoveryParticipant.java
new file mode 100644
index 0000000000000..098da525d6096
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/discovery/Enigma2DiscoveryParticipant.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal.discovery;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import javax.jmdns.ServiceInfo;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.eclipse.smarthome.config.discovery.mdns.MDNSDiscoveryParticipant;
+import org.openhab.binding.enigma2.internal.Enigma2BindingConstants;
+import org.openhab.binding.enigma2.internal.Enigma2HttpClient;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Enigma2DiscoveryParticipant} is responsible processing the
+ * results of searches for mDNS services of type _http._tcp.local. and finding a webinterface
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = MDNSDiscoveryParticipant.class, immediate = true)
+public class Enigma2DiscoveryParticipant implements MDNSDiscoveryParticipant {
+
+ private final Logger logger = LoggerFactory.getLogger(Enigma2DiscoveryParticipant.class);
+
+ @Override
+ public Set getSupportedThingTypeUIDs() {
+ return Enigma2BindingConstants.SUPPORTED_THING_TYPES_UIDS;
+ }
+
+ @Override
+ public @Nullable DiscoveryResult createResult(ServiceInfo info) {
+ logger.debug("ServiceInfo {}", info);
+ String ipAddress = getIPAddress(info);
+ if (ipAddress != null && isEnigma2Device(ipAddress)) {
+ logger.debug("Enigma2 device discovered: IP-Adress={}, name={}", ipAddress, info.getName());
+ ThingUID uid = getThingUID(info);
+ if(uid != null) {
+ Map properties = new HashMap<>();
+ properties.put(Enigma2BindingConstants.CONFIG_HOST, ipAddress);
+ properties.put(Enigma2BindingConstants.CONFIG_REFRESH, 5);
+ properties.put(Enigma2BindingConstants.CONFIG_TIMEOUT, 5);
+ return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(info.getName()).build();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public @Nullable ThingUID getThingUID(ServiceInfo info) {
+ logger.debug("ServiceInfo {}", info);
+ String ipAddress = getIPAddress(info);
+ if( ipAddress != null) {
+ return new ThingUID(Enigma2BindingConstants.THING_TYPE_DEVICE, ipAddress.replace(".", "_"));
+ }
+ return null;
+ }
+
+ @Override
+ public String getServiceType() {
+ return "_http._tcp.local.";
+ }
+
+ private boolean isEnigma2Device(String ipAddress) {
+ try {
+ return getEnigma2HttpClient().get("http://" + ipAddress + "/web/about").contains("e2enigmaversion");
+ } catch (IOException ignore) {
+ return false;
+ }
+ }
+
+ private @Nullable String getIPAddress(ServiceInfo info) {
+ InetAddress[] addresses = info.getInet4Addresses();
+ if (addresses.length > 1) {
+ logger.debug("Enigma2 device {} reports multiple addresses - using the first one! {}", info.getName(), addresses);
+ }
+ return Stream.of(addresses).findFirst().map(InetAddress::getHostAddress).orElse(null);
+ }
+
+ /**
+ * Getter for Test-Injection
+ *
+ * @return HttpGet.
+ */
+ Enigma2HttpClient getEnigma2HttpClient() {
+ return new Enigma2HttpClient(5);
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..d6906c6b8a1b4
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ Enigma2 Binding
+ This is the binding for Enigma2.
+ Guido Dolfen
+
+
diff --git a/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/i18n/enigma2.properties b/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/i18n/enigma2.properties
new file mode 100644
index 0000000000000..3eca7bac7b98d
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/i18n/enigma2.properties
@@ -0,0 +1,36 @@
+# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE
+# FIXME: please do not add the file to the repo if you add or change no content
+# binding
+binding.enigma2.name = Enigma2 Binding
+binding.enigma2.description = This is the binding for Enigma2
+
+# thing types
+thing-type.enigma2.device.label = Enigma2
+thing-type.enigma2.device.description = The Thing represents an Enigma2 device
+
+# thing type config description
+thing-type.config.enigma2.host.label = Host Address
+thing-type.config.enigma2.host.description = Hostname or IP address of the Enigma2 device
+
+# channel types
+channel-type.enigma2.power.label = Power
+channel-type.enigma2.power.description = Setting the power to on/off.
+
+# actions
+action.enigma2.send-rc-button.label=sendRcCommand
+action.enigma2.send-rc-button.description=Send an Remote Control Command
+action-input.enigma2.rc-button.label=rcButton
+action-input.enigma2.rc-button.description=The Remote Control Button
+
+action.enigma2.send-info.label=sendInfo
+action.enigma2.send-info.description=Send an info message to the TV screen
+action.enigma2.send-warning.label=sendWarning
+action.enigma2.send-warning.description=Send an warning message to the TV screen
+action.enigma2.send-error.label=sendError
+action.enigma2.send-error.description=Send an error message to the TV screen
+action.enigma2.send-question.label=sendQuestion
+action.enigma2.send-question.description=Send a question message to the TV screen
+action-input.enigma2.text.label=text
+action-input.enigma2.text.description=The message text
+action-input.enigma2.timeout.label=timeout
+action-input.enigma2.timeout.description=The timeout in seconds
diff --git a/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..c52f4408f34ed
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/thing/thing-types.xml
@@ -0,0 +1,99 @@
+
+
+
+
+ The Thing represents an Enigma2 device
+
+
+
+
+
+
+
+
+
+
+
+
+
+ network-address
+
+ Hostname or IP address of the Enigma2 device.
+
+
+
+ The refresh interval in seconds.
+ 5
+
+
+
+ The timeout for reading from the device in seconds.
+ 5
+
+
+
+ The Username of the Enigma2 Web API.
+
+
+ password
+
+ The Password of the Enigma2 Web API.
+
+
+
+
+ Switch
+
+ Setting the power to on/off.
+
+
+ Switch
+
+ Current Mute Setting
+ SoundVolume
+
+
+ Dimmer
+
+ Current Volume Setting
+ SoundVolume
+
+
+
+ String
+
+ Current Channel
+
+
+ String
+
+ Current Title of the current Channel
+
+
+
+ String
+
+ Current Description of the current Channel
+
+
+
+ Player
+
+ Control media (e.g. audio or video) playback
+ MediaControl
+
+
+ Switch
+
+ Stop Playback
+
+
+ String
+
+ Receives an answer to a send question of the device
+
+
+
diff --git a/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/actions/Enigma2ActionsTest.java b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/actions/Enigma2ActionsTest.java
new file mode 100644
index 0000000000000..eadf7465877c5
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/actions/Enigma2ActionsTest.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.actions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import org.openhab.binding.enigma2.handler.Enigma2Handler;
+import org.openhab.binding.enigma2.internal.Enigma2BindingConstants;
+
+/**
+ * The {@link Enigma2ActionsTest} class is responsible for testing {@link Enigma2Actions}.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@SuppressWarnings("null")
+@NonNullByDefault
+public class Enigma2ActionsTest {
+ @Nullable
+ private Enigma2Actions enigma2Actions;
+ @Nullable
+ private Enigma2Handler enigma2Handler;
+ public static final String SOME_TEXT = "some Text";
+
+ @Before
+ public void setUp() {
+ enigma2Handler = mock(Enigma2Handler.class);
+ enigma2Actions = new Enigma2Actions();
+ enigma2Actions.setThingHandler(enigma2Handler);
+ }
+
+ @Test
+ public void testGetThingHandler() {
+ assertThat(enigma2Actions.getThingHandler(), is(enigma2Handler));
+ }
+
+ @Test
+ public void testSendRcCommand() {
+ enigma2Actions.sendRcCommand("KEY_1");
+ verify(enigma2Handler).sendRcCommand("KEY_1");
+ }
+
+ @Test
+ public void testSendInfo() {
+ enigma2Actions.sendInfo(SOME_TEXT);
+ verify(enigma2Handler).sendInfo(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendInfoTimeout() {
+ enigma2Actions.sendInfo(SOME_TEXT, 10);
+ verify(enigma2Handler).sendInfo(10, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendError() {
+ enigma2Actions.sendError(SOME_TEXT);
+ verify(enigma2Handler).sendError(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendErrorTimeout() {
+ enigma2Actions.sendError(SOME_TEXT, 10);
+ verify(enigma2Handler).sendError(10, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendWarning() {
+ enigma2Actions.sendWarning(SOME_TEXT);
+ verify(enigma2Handler).sendWarning(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendWarningTimeout() {
+ enigma2Actions.sendWarning(SOME_TEXT, 10);
+ verify(enigma2Handler).sendWarning(10, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendQuestion() {
+ enigma2Actions.sendQuestion(SOME_TEXT);
+ verify(enigma2Handler).sendQuestion(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendQuestionTimeout() {
+ enigma2Actions.sendQuestion(SOME_TEXT, 10);
+ verify(enigma2Handler).sendQuestion(10, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendRcCommandStatic() {
+ Enigma2Actions.sendRcCommand(enigma2Actions, "KEY_1");
+ verify(enigma2Handler).sendRcCommand("KEY_1");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSendRcCommandStaticWithException() {
+ Enigma2Actions.sendRcCommand(null, "KEY_1");
+ }
+
+ @Test
+ public void testSendInfoStatic() {
+ Enigma2Actions.sendInfo(enigma2Actions, SOME_TEXT);
+ verify(enigma2Handler).sendInfo(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendInfoTimeoutStatic() {
+ Enigma2Actions.sendInfo(enigma2Actions, SOME_TEXT, 10);
+ verify(enigma2Handler).sendInfo(10, SOME_TEXT);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSendInfoStaticWithException() {
+ Enigma2Actions.sendInfo(null, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendErrorStatic() {
+ Enigma2Actions.sendError(enigma2Actions, SOME_TEXT);
+ verify(enigma2Handler).sendError(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendErrorTimeoutStatic() {
+ Enigma2Actions.sendError(enigma2Actions, SOME_TEXT, 10);
+ verify(enigma2Handler).sendError(10, SOME_TEXT);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSendErrorStaticWithException() {
+ Enigma2Actions.sendError(null, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendWarningStatic() {
+ Enigma2Actions.sendWarning(enigma2Actions, SOME_TEXT);
+ verify(enigma2Handler).sendWarning(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendWarningTimeoutStatic() {
+ Enigma2Actions.sendWarning(enigma2Actions, SOME_TEXT, 10);
+ verify(enigma2Handler).sendWarning(10, SOME_TEXT);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSendWarningStaticWithException() {
+ Enigma2Actions.sendWarning(null, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendQuestionStatic() {
+ Enigma2Actions.sendQuestion(enigma2Actions, SOME_TEXT);
+ verify(enigma2Handler).sendQuestion(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendQuestionTimeoutStatic() {
+ Enigma2Actions.sendQuestion(enigma2Actions, SOME_TEXT, 10);
+ verify(enigma2Handler).sendQuestion(10, SOME_TEXT);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSendQuestionStaticWithException() {
+ Enigma2Actions.sendQuestion(null, SOME_TEXT);
+ }
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/handler/Enigma2HandlerTest.java b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/handler/Enigma2HandlerTest.java
new file mode 100644
index 0000000000000..657a84e09b9d5
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/handler/Enigma2HandlerTest.java
@@ -0,0 +1,381 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.core.Configuration;
+import org.eclipse.smarthome.core.library.types.*;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerCallback;
+import org.eclipse.smarthome.core.types.RefreshType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.enigma2.actions.Enigma2Actions;
+import org.openhab.binding.enigma2.internal.Enigma2BindingConstants;
+import org.openhab.binding.enigma2.internal.Enigma2Client;
+import org.openhab.binding.enigma2.internal.Enigma2Configuration;
+import org.openhab.binding.enigma2.internal.Enigma2RemoteKey;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+
+import static org.eclipse.jdt.annotation.Checks.requireNonNull;
+import static org.mockito.Mockito.*;
+import static org.junit.Assert.*;
+import static org.hamcrest.Matchers.*;
+
+/**
+ * The {@link Enigma2HandlerTest} class is responsible for testing {@link Enigma2Handler}.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@SuppressWarnings({ "null", "unchecked" })
+@NonNullByDefault
+public class Enigma2HandlerTest {
+ public static final String CHANNEL_UID_PREFIX = "enigma2:device:192_168_0_3:";
+ public static final String SOME_TEXT = "some Text";
+ @Nullable
+ private Enigma2Handler enigma2Handler;
+ @Nullable
+ private Enigma2Client enigma2Client;
+ @Nullable
+ private Thing thing;
+ @Nullable
+ private Configuration configuration;
+ @Nullable
+ private ThingHandlerCallback callback;
+
+ @Before
+ public void setUp() {
+ enigma2Client = mock(Enigma2Client.class);
+ thing = mock(Thing.class);
+ callback = mock(ThingHandlerCallback.class);
+ configuration = mock(Configuration.class);
+ when(thing.getConfiguration()).thenReturn(requireNonNull(configuration));
+ when(configuration.as(Enigma2Configuration.class)).thenReturn(new Enigma2Configuration());
+ enigma2Handler = spy(new Enigma2Handler(requireNonNull(thing)));
+ enigma2Handler.setCallback(callback);
+ when(enigma2Handler.getEnigma2Client()).thenReturn(Optional.of(requireNonNull(enigma2Client)));
+ }
+
+ @Test
+ public void testSendRcCommand() {
+ enigma2Handler.sendRcCommand("KEY_1");
+ verify(enigma2Client).sendRcCommand(Enigma2RemoteKey.KEY_1.getValue());
+ }
+
+ @Test
+ public void testSendInfo() {
+ enigma2Handler.sendInfo(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ verify(enigma2Client).sendInfo(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendWarning() {
+ enigma2Handler.sendWarning(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ verify(enigma2Client).sendWarning(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendError() {
+ enigma2Handler.sendError(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ verify(enigma2Client).sendError(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testSendQuestion() {
+ enigma2Handler.sendQuestion(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ verify(enigma2Client).sendQuestion(Enigma2BindingConstants.MESSAGE_TIMEOUT, SOME_TEXT);
+ }
+
+ @Test
+ public void testGetEnigma2Client() {
+ enigma2Handler = new Enigma2Handler(requireNonNull(thing));
+ assertThat(enigma2Handler.getEnigma2Client(), is(Optional.empty()));
+ }
+
+ @Test
+ public void testGetServices() {
+ enigma2Handler = new Enigma2Handler(requireNonNull(thing));
+ assertThat(enigma2Handler.getServices(), contains(Enigma2Actions.class));
+ }
+
+ @Test
+ public void testSendRcCommandUnsupported() {
+ enigma2Handler.sendRcCommand("KEY_X");
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandPowerRefreshFalse() {
+ when(enigma2Client.isPower()).thenReturn(false);
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_POWER);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshPower();
+ verify(callback).stateUpdated(channelUID, OnOffType.OFF);
+ }
+
+ @Test
+ public void testHandleCommandPowerRefreshTrue() {
+ when(enigma2Client.isPower()).thenReturn(true);
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_POWER);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshPower();
+ verify(callback).stateUpdated(channelUID, OnOffType.ON);
+ }
+
+ @Test
+ public void testHandleCommandPowerOn() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_POWER);
+ enigma2Handler.handleCommand(channelUID, OnOffType.ON);
+ verify(enigma2Client).setPower(true);
+ }
+
+ @Test
+ public void testHandleCommandPowerOff() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_POWER);
+ enigma2Handler.handleCommand(channelUID, OnOffType.OFF);
+ verify(enigma2Client).setPower(false);
+ }
+
+ @Test
+ public void testHandleCommandPowerUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_POWER);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandChannelRefresh() {
+ when(enigma2Client.getChannel()).thenReturn(SOME_TEXT);
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_CHANNEL);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshChannel();
+ verify(callback).stateUpdated(channelUID, new StringType(SOME_TEXT));
+ }
+
+ @Test
+ public void testHandleCommandMuteRefreshFalse() {
+ when(enigma2Client.isMute()).thenReturn(false);
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MUTE);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshVolume();
+ verify(callback).stateUpdated(channelUID, OnOffType.OFF);
+ }
+
+ @Test
+ public void testHandleCommandMuteRefreshTrue() {
+ when(enigma2Client.isMute()).thenReturn(true);
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MUTE);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshVolume();
+ verify(callback).stateUpdated(channelUID, OnOffType.ON);
+ }
+
+ @Test
+ public void testHandleCommandMuteOn() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MUTE);
+ enigma2Handler.handleCommand(channelUID, OnOffType.ON);
+ verify(enigma2Client).setMute(true);
+ }
+
+ @Test
+ public void testHandleCommandMuteOff() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MUTE);
+ enigma2Handler.handleCommand(channelUID, OnOffType.OFF);
+ verify(enigma2Client).setMute(false);
+ }
+
+ @Test
+ public void testHandleCommandMuteUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MUTE);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandChannelString() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_CHANNEL);
+ enigma2Handler.handleCommand(channelUID, new StringType(SOME_TEXT));
+ verify(enigma2Client).setChannel(SOME_TEXT);
+ }
+
+ @Test
+ public void testHandleCommandChannelUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_CHANNEL);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandMediaPlayerRefresh() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_PLAYER);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandMediaPlay() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_PLAYER);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PLAY);
+ verify(enigma2Client).sendRcCommand(Enigma2RemoteKey.PLAY.getValue());
+ }
+
+ @Test
+ public void testHandleCommandMediaPause() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_PLAYER);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verify(enigma2Client).sendRcCommand(Enigma2RemoteKey.PAUSE.getValue());
+ }
+
+ @Test
+ public void testHandleCommandMediaNext() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_PLAYER);
+ enigma2Handler.handleCommand(channelUID, NextPreviousType.NEXT);
+ verify(enigma2Client).sendRcCommand(Enigma2RemoteKey.FAST_FORWARD.getValue());
+ }
+
+ @Test
+ public void testHandleCommandMediaPrevious() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_PLAYER);
+ enigma2Handler.handleCommand(channelUID, NextPreviousType.PREVIOUS);
+ verify(enigma2Client).sendRcCommand(Enigma2RemoteKey.FAST_BACKWARD.getValue());
+ }
+
+ @Test
+ public void testHandleCommandMediaPlayerUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_PLAYER);
+ enigma2Handler.handleCommand(channelUID, OnOffType.ON);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandMediaStopRefresh() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_STOP);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandMediaStopOn() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_STOP);
+ enigma2Handler.handleCommand(channelUID, OnOffType.ON);
+ verify(enigma2Client).sendRcCommand(Enigma2RemoteKey.STOP.getValue());
+ }
+
+ @Test
+ public void testHandleCommandMediaStopOff() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_STOP);
+ enigma2Handler.handleCommand(channelUID, OnOffType.OFF);
+ verify(enigma2Client).sendRcCommand(Enigma2RemoteKey.STOP.getValue());
+ }
+
+ @Test
+ public void testHandleCommandMediaStopUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_MEDIA_STOP);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandTitleUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_TITLE);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandTitleRefresh() {
+ when(enigma2Client.getTitle()).thenReturn(SOME_TEXT);
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_TITLE);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshEpg();
+ verify(callback).stateUpdated(channelUID, new StringType(SOME_TEXT));
+ }
+
+ @Test
+ public void testHandleCommandAnswerUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_ANSWER);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandAnswerRefresh() {
+ when(enigma2Client.getAnswer()).thenReturn(SOME_TEXT);
+ when(enigma2Client.getLastAnswerTime()).thenReturn(LocalDateTime.now().plus(1, ChronoUnit.SECONDS));
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_ANSWER);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshAnswer();
+ verify(callback).stateUpdated(channelUID, new StringType(SOME_TEXT));
+ }
+
+ @Test
+ public void testHandleCommandAnswerRefreshFalse() {
+ when(enigma2Client.getAnswer()).thenReturn(SOME_TEXT);
+ when(enigma2Client.getLastAnswerTime()).thenReturn(LocalDateTime.of(2020, 1, 1, 0, 0));
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_ANSWER);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshAnswer();
+ verifyNoInteractions(callback);
+ }
+
+ @Test
+ public void testHandleCommandDescriptionUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_DESCRIPTION);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verifyNoInteractions(enigma2Client);
+ }
+
+ @Test
+ public void testHandleCommandDescriptionRefresh() {
+ when(enigma2Client.getDescription()).thenReturn(SOME_TEXT);
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_DESCRIPTION);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshEpg();
+ verify(callback).stateUpdated(channelUID, new StringType(SOME_TEXT));
+ }
+
+ @Test
+ public void testHandleCommandVolumeRefresh() {
+ when(enigma2Client.getVolume()).thenReturn(35);
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_VOLUME);
+ enigma2Handler.handleCommand(channelUID, RefreshType.REFRESH);
+ verify(enigma2Client).refreshVolume();
+ verify(callback).stateUpdated(channelUID, new PercentType(35));
+ }
+
+ @Test
+ public void testHandleCommandVolumePercent() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_VOLUME);
+ enigma2Handler.handleCommand(channelUID, new PercentType(30));
+ verify(enigma2Client).setVolume(30);
+ }
+
+ @Test
+ public void testHandleCommandVolumeDecimal() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_VOLUME);
+ enigma2Handler.handleCommand(channelUID, new DecimalType(40));
+ verify(enigma2Client).setVolume(40);
+ }
+
+ @Test
+ public void testHandleCommandVolumeUnsupported() {
+ ChannelUID channelUID = new ChannelUID(CHANNEL_UID_PREFIX + Enigma2BindingConstants.CHANNEL_VOLUME);
+ enigma2Handler.handleCommand(channelUID, PlayPauseType.PAUSE);
+ verifyNoInteractions(enigma2Client);
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2ClientTest.java b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2ClientTest.java
new file mode 100644
index 0000000000000..c38e07b187f81
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2ClientTest.java
@@ -0,0 +1,323 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+
+import static org.eclipse.jdt.annotation.Checks.requireNonNull;
+import static org.mockito.Mockito.*;
+import static org.junit.Assert.*;
+import static org.hamcrest.Matchers.*;
+
+/**
+ * The {@link Enigma2ClientTest} class is responsible for testing {@link Enigma2Client}.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@SuppressWarnings({ "null" })
+@NonNullByDefault
+public class Enigma2ClientTest {
+ public static final String HOST = "http://user:password@localhost:8080";
+ public static final String SOME_TEXT = "some Text";
+ public static final String SOME_TEXT_ENCODED = "some+Text";
+ @Nullable
+ private Enigma2Client enigma2Client;
+ @Nullable
+ private Enigma2HttpClient enigma2HttpClient;
+
+ @Before
+ public void setUp() throws IOException {
+ enigma2HttpClient = mock(Enigma2HttpClient.class);
+ enigma2Client = spy(new Enigma2Client("localhost:8080", "user", "password", 5));
+ when(enigma2Client.getEnigma2HttpClient()).thenReturn(requireNonNull(enigma2HttpClient));
+ when(enigma2HttpClient.get(anyString())).thenReturn("");
+ }
+
+ @Test
+ public void testSetPowerFalse() throws IOException {
+ whenStandby("true");
+ enigma2Client.setPower(false);
+ verify(enigma2HttpClient).get(HOST + Enigma2Client.PATH_POWER);
+ verifyNoMoreInteractions(enigma2HttpClient);
+ }
+
+ @Test
+ public void testSetPower() throws IOException {
+ whenStandby("true");
+ enigma2Client.setPower(true);
+ verify(enigma2HttpClient).get(HOST + Enigma2Client.PATH_TOGGLE_POWER);
+ }
+
+ @Test
+ public void testSetVolume() throws IOException {
+ enigma2Client.setVolume(20);
+ verify(enigma2HttpClient).get(HOST + Enigma2Client.PATH_SET_VOLUME + 20);
+ }
+
+ @Test
+ public void testSetChannel() throws IOException {
+ whenStandby("false");
+ whenAllServices();
+ enigma2Client.refreshPower();
+ enigma2Client.refreshAllServices();
+ enigma2Client.setChannel("Channel 3");
+ verify(enigma2HttpClient).get(HOST + Enigma2Client.PATH_ZAP + 3);
+ }
+
+ @Test
+ public void testSetChannelUnknown() throws IOException {
+ enigma2Client.setChannel("Channel 3");
+ verifyNoInteractions(enigma2HttpClient);
+ }
+
+ @Test
+ public void testSetMuteFalse() throws IOException {
+ whenStandby("false");
+ whenVolume("10", false);
+ enigma2Client.refreshPower();
+ enigma2Client.setMute(false);
+ verify(enigma2HttpClient).get(HOST + Enigma2Client.PATH_POWER);
+ verify(enigma2HttpClient).get(HOST + Enigma2Client.PATH_VOLUME);
+ verifyNoMoreInteractions(enigma2HttpClient);
+ }
+
+ @Test
+ public void testSetMute() throws IOException {
+ whenStandby("false");
+ whenVolume("10", false);
+ enigma2Client.refreshPower();
+ enigma2Client.setMute(true);
+ verify(enigma2HttpClient).get(HOST + Enigma2Client.PATH_TOGGLE_MUTE);
+ }
+
+ @Test
+ public void testSendRcCommand() throws IOException {
+ enigma2Client.sendRcCommand(2);
+ verify(enigma2HttpClient).get(HOST + Enigma2Client.PATH_REMOTE_CONTROL + 2);
+ }
+
+ @Test
+ public void testSendError() throws IOException {
+ enigma2Client.sendError(20, SOME_TEXT);
+ verify(enigma2HttpClient).get(HOST + "/web/message?type=3&timeout=20&text=" + SOME_TEXT_ENCODED);
+ }
+
+ @Test
+ public void testSendWarning() throws IOException {
+ enigma2Client.sendWarning(35, SOME_TEXT);
+ verify(enigma2HttpClient).get(HOST + "/web/message?type=2&timeout=35&text=" + SOME_TEXT_ENCODED);
+ }
+
+ @Test
+ public void testSendInfo() throws IOException {
+ enigma2Client.sendInfo(40, SOME_TEXT);
+ verify(enigma2HttpClient).get(HOST + "/web/message?type=1&timeout=40&text=" + SOME_TEXT_ENCODED);
+ }
+
+ @Test
+ public void testSendQuestion() throws IOException {
+ enigma2Client.sendQuestion(50, SOME_TEXT);
+ verify(enigma2HttpClient).get(HOST + "/web/message?type=0&timeout=50&text=" + SOME_TEXT_ENCODED);
+ }
+
+ @Test
+ public void testRefreshPowerTrue() throws IOException {
+ whenStandby(" FALSE ");
+ enigma2Client.refreshPower();
+ assertThat(enigma2Client.isPower(), is(true));
+ }
+
+ @Test
+ public void testRefreshVolumeMuteTrue() throws IOException {
+ whenStandby("false");
+ whenVolume("30", true);
+ enigma2Client.refreshPower();
+ enigma2Client.refreshVolume();
+ assertThat(enigma2Client.isMute(), is(true));
+ assertThat(enigma2Client.getVolume(), is(30));
+ }
+
+ @Test
+ public void testRefreshVolumeMuteFalse() throws IOException {
+ whenStandby("false");
+ whenVolume("30", false);
+ enigma2Client.refreshPower();
+ enigma2Client.refreshVolume();
+ assertThat(enigma2Client.isMute(), is(false));
+ assertThat(enigma2Client.getVolume(), is(30));
+ }
+
+ @Test
+ public void testRefreshVolumePowerOff() throws IOException {
+ enigma2Client.refreshVolume();
+ assertThat(enigma2Client.isMute(), is(false));
+ assertThat(enigma2Client.getVolume(), is(0));
+ }
+
+ @Test
+ public void testRefreshPowerFalse() throws IOException {
+ whenStandby(" TRUE ");
+ enigma2Client.refreshPower();
+ assertThat(enigma2Client.isPower(), is(false));
+ }
+
+ @Test
+ public void testRefreshPowerOffline() throws IOException {
+ IOException ioException = new IOException();
+ when(enigma2HttpClient.get(HOST + Enigma2Client.PATH_POWER)).thenThrow(ioException);
+ enigma2Client.refreshPower();
+ assertThat(enigma2Client.isPower(), is(false));
+ }
+
+ @Test
+ public void testRefreshAllServices() throws IOException {
+ whenStandby("false");
+ whenAllServices();
+ enigma2Client.refreshAllServices();
+ assertThat(enigma2Client.getChannels(), containsInAnyOrder("Channel 1", "Channel 2", "Channel 3"));
+ }
+
+ @Test
+ public void testRefreshChannel() throws IOException {
+ whenStandby("false");
+ whenChannel("2", "Channel 2");
+ enigma2Client.refreshPower();
+ enigma2Client.refreshChannel();
+ assertThat(enigma2Client.getChannel(), is("Channel 2"));
+ }
+
+ @Test
+ public void testRefreshEpg() throws IOException {
+ whenStandby("false");
+ whenAllServices();
+ whenChannel("2", "Channel 2");
+ whenEpg("2", "Title", "Description");
+ enigma2Client.refreshPower();
+ enigma2Client.refreshAllServices();
+ enigma2Client.refreshChannel();
+ enigma2Client.refreshEpg();
+ assertThat(enigma2Client.getTitle(), is("Title"));
+ assertThat(enigma2Client.getDescription(), is("Description"));
+ }
+
+ @Test
+ public void testRefreshAnswerTimeout() throws IOException {
+ whenStandby("false");
+ whenAnswer("False", "Timeout");
+ enigma2Client.refreshPower();
+ enigma2Client.refreshAnswer();
+ assertThat(enigma2Client.getLastAnswerTime().isAfter(LocalDateTime.of(2020, 1, 1, 0, 0)), is(false));
+ assertThat(enigma2Client.getAnswer(), is(""));
+ }
+
+ @Test
+ public void testRefreshAnswerNoQuestion() throws IOException {
+ whenStandby("false");
+ whenAnswer("True", "Antwort lautet NEIN!");
+ enigma2Client.refreshPower();
+ enigma2Client.refreshAnswer();
+ assertThat(enigma2Client.getLastAnswerTime().isAfter(LocalDateTime.of(2020, 1, 1, 0, 0)), is(false));
+ assertThat(enigma2Client.getAnswer(), is(""));
+ }
+
+ @Test
+ public void testRefreshAnswer() throws IOException {
+ whenStandby("false");
+ whenAnswer("True", "Antwort lautet NEIN!");
+ enigma2Client.refreshPower();
+ enigma2Client.sendQuestion(50, SOME_TEXT);
+ enigma2Client.refreshAnswer();
+ assertThat(enigma2Client.getLastAnswerTime().isAfter(LocalDateTime.of(2020, 1, 1, 0, 0)), is(true));
+ assertThat(enigma2Client.getAnswer(), is("NEIN"));
+ }
+
+ @Test
+ public void testRefresh() throws IOException {
+ whenStandby("false");
+ whenAllServices();
+ whenVolume("A", false);
+ whenChannel("1", "Channel 1");
+ whenEpg("1", "Title", "Description");
+ assertThat(enigma2Client.refresh(), is(true));
+ assertThat(enigma2Client.isPower(), is(true));
+ assertThat(enigma2Client.isMute(), is(false));
+ assertThat(enigma2Client.getVolume(), is(0));
+ assertThat(enigma2Client.getChannel(), is("Channel 1"));
+ assertThat(enigma2Client.getTitle(), is("Title"));
+ assertThat(enigma2Client.getDescription(), is("Description"));
+ assertThat(enigma2Client.getChannels(), containsInAnyOrder("Channel 1", "Channel 2", "Channel 3"));
+ }
+
+ @Test
+ public void testRefreshOffline() throws IOException {
+ IOException ioException = new IOException();
+ when(enigma2HttpClient.get(HOST + Enigma2Client.PATH_POWER)).thenThrow(ioException);
+ assertThat(enigma2Client.refresh(), is(false));
+ assertThat(enigma2Client.isPower(), is(false));
+ assertThat(enigma2Client.isMute(), is(false));
+ assertThat(enigma2Client.getVolume(), is(0));
+ assertThat(enigma2Client.getChannel(), is(""));
+ assertThat(enigma2Client.getTitle(), is(""));
+ assertThat(enigma2Client.getDescription(), is(""));
+ assertThat(enigma2Client.getChannels().isEmpty(), is(true));
+ }
+
+ @Test
+ public void testGetEnigma2HttpClient() {
+ enigma2Client = new Enigma2Client("http://localhost:8080", null, null, 5);
+ assertThat(enigma2Client.getEnigma2HttpClient(), is(notNullValue()));
+ }
+
+ private void whenVolume(String volume, boolean mute) throws IOException {
+ when(enigma2HttpClient.get(HOST + Enigma2Client.PATH_VOLUME)).thenReturn(
+ "" + volume + "" + mute + "");
+ }
+
+ private void whenEpg(String id, String title, String description) throws IOException {
+ when(enigma2HttpClient.get(HOST + Enigma2Client.PATH_EPG + id)).thenReturn("" + title
+ + "" + description + "");
+ }
+
+ private void whenAnswer(String state, String answer) throws IOException {
+ when(enigma2HttpClient.get(HOST + Enigma2Client.PATH_ANSWER)).thenReturn("" + state
+ + "" + answer + "");
+ }
+
+ private void whenStandby(String standby) throws IOException {
+ when(enigma2HttpClient.get(HOST + Enigma2Client.PATH_POWER))
+ .thenReturn("" + standby + "");
+ }
+
+ private void whenChannel(String id, String name) throws IOException {
+ when(enigma2HttpClient.get(HOST + Enigma2Client.PATH_CHANNEL)).thenReturn(
+ "" + id + "" + name
+ + "");
+ }
+
+ private void whenAllServices() throws IOException {
+ when(enigma2HttpClient.get(HOST + Enigma2Client.PATH_ALL_SERVICES))
+ .thenReturn("" + "" + "" + ""
+ + "1" + "Channel 1"
+ + "" + "" + "2"
+ + "Channel 2" + "" + ""
+ + "" + "" + "" + ""
+ + "3" + "Channel 3"
+ + "" + "" + "" + "");
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2HandlerFactoryTest.java b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2HandlerFactoryTest.java
new file mode 100644
index 0000000000000..e8a55a92a9402
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2HandlerFactoryTest.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.core.Configuration;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.junit.Test;
+
+import static org.eclipse.jdt.annotation.Checks.requireNonNull;
+import static org.junit.Assert.*;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.openhab.binding.enigma2.internal.Enigma2BindingConstants.THING_TYPE_DEVICE;
+
+/**
+ * The {@link Enigma2HandlerFactoryTest} class is responsible for testing {@link Enigma2HandlerFactory}.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@SuppressWarnings("null")
+@NonNullByDefault
+public class Enigma2HandlerFactoryTest {
+ @Nullable
+ private Thing thing;
+ @Nullable
+ private Configuration configuration;
+
+ @Test
+ public void testSupportsThingType() {
+ assertThat(new Enigma2HandlerFactory().supportsThingType(Enigma2BindingConstants.THING_TYPE_DEVICE), is(true));
+ }
+
+ @Test
+ public void testSupportsThingTypeFalse() {
+ assertThat(new Enigma2HandlerFactory().supportsThingType(new ThingTypeUID("any", "device")), is(false));
+ }
+
+ @Test
+ public void testCreateHandlerNull() {
+ thing = mock(Thing.class);
+ assertThat(new Enigma2HandlerFactory().createHandler(requireNonNull(thing)), is(nullValue()));
+ }
+
+ @Test
+ public void testCreateHandler() {
+ thing = mock(Thing.class);
+ configuration = mock(Configuration.class);
+ when(thing.getConfiguration()).thenReturn(requireNonNull(configuration));
+ when(configuration.as(Enigma2Configuration.class)).thenReturn(new Enigma2Configuration());
+ when(thing.getThingTypeUID()).thenReturn(THING_TYPE_DEVICE);
+ assertThat(new Enigma2HandlerFactory().createHandler(requireNonNull(thing)), is(notNullValue()));
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2RemoteKeyTest.java b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2RemoteKeyTest.java
new file mode 100644
index 0000000000000..ea5188dc2470a
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2RemoteKeyTest.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.hamcrest.Matchers.*;
+
+/**
+ * The {@link Enigma2RemoteKeyTest} class is responsible for testing {@link Enigma2RemoteKey}.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@NonNullByDefault
+public class Enigma2RemoteKeyTest {
+ @Test
+ public void testGetValue() {
+ assertThat(Enigma2RemoteKey.ARROW_LEFT.getValue(), is(412));
+ }
+}
diff --git a/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/discovery/Enigma2DiscoveryParticipantTest.java b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/discovery/Enigma2DiscoveryParticipantTest.java
new file mode 100644
index 0000000000000..12d1f74ddd970
--- /dev/null
+++ b/bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/discovery/Enigma2DiscoveryParticipantTest.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.enigma2.internal.discovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.enigma2.internal.Enigma2BindingConstants;
+import org.openhab.binding.enigma2.internal.Enigma2HttpClient;
+
+import javax.jmdns.ServiceInfo;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+import static org.eclipse.jdt.annotation.Checks.requireNonNull;
+import static org.junit.Assert.*;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.when;
+
+/**
+ * The {@link Enigma2DiscoveryParticipantTest} class is responsible for testing {@link Enigma2DiscoveryParticipant}.
+ *
+ * @author Guido Dolfen - Initial contribution
+ */
+@SuppressWarnings({ "null" })
+@NonNullByDefault
+public class Enigma2DiscoveryParticipantTest {
+ @Nullable
+ private ServiceInfo serviceInfo;
+ @Nullable
+ private Enigma2HttpClient enigma2HttpClient;
+ @Nullable
+ private Enigma2DiscoveryParticipant enigma2DiscoveryParticipant;
+
+ @Before
+ public void setUp() {
+ enigma2HttpClient = mock(Enigma2HttpClient.class);
+ serviceInfo = mock(ServiceInfo.class);
+ enigma2DiscoveryParticipant = spy(new Enigma2DiscoveryParticipant());
+ when(enigma2DiscoveryParticipant.getEnigma2HttpClient()).thenReturn(requireNonNull(enigma2HttpClient));
+ }
+
+ @Test
+ public void testGetSupportedThingTypeUIDs() {
+ assertThat(enigma2DiscoveryParticipant.getSupportedThingTypeUIDs(),
+ contains(Enigma2BindingConstants.THING_TYPE_DEVICE));
+ }
+
+ @Test
+ public void testGetServiceType() {
+ assertThat(enigma2DiscoveryParticipant.getServiceType(), is("_http._tcp.local."));
+ }
+
+ @Test
+ public void testCreateResult() throws Exception {
+ when(serviceInfo.getName()).thenReturn("enigma2");
+ when(enigma2HttpClient.get("http://192.168.10.3/web/about"))
+ .thenReturn("2020-01-11");
+ when(serviceInfo.getInet4Addresses())
+ .thenReturn(new Inet4Address[] { (Inet4Address) InetAddress.getAllByName("192.168.10.3")[0] });
+ DiscoveryResult discoveryResult = enigma2DiscoveryParticipant.createResult(requireNonNull(serviceInfo));
+ assertThat(discoveryResult, is(notNullValue()));
+ assertThat(discoveryResult.getLabel(), is("enigma2"));
+ assertThat(discoveryResult.getThingUID(),
+ is(new ThingUID(Enigma2BindingConstants.THING_TYPE_DEVICE, "192_168_10_3")));
+ assertThat(discoveryResult.getProperties(), is(notNullValue()));
+ assertThat(discoveryResult.getProperties(), hasEntry(Enigma2BindingConstants.CONFIG_HOST, "192.168.10.3"));
+ assertThat(discoveryResult.getProperties(),
+ hasEntry(Enigma2BindingConstants.CONFIG_REFRESH, 5));
+ assertThat(discoveryResult.getProperties(),
+ hasEntry(Enigma2BindingConstants.CONFIG_TIMEOUT, 5));
+ }
+
+ @Test
+ public void testCreateResultNotFound() throws Exception {
+ when(enigma2HttpClient.get("http://192.168.10.3/web/about")).thenReturn("any");
+ when(serviceInfo.getInet4Addresses())
+ .thenReturn(new Inet4Address[] { (Inet4Address) InetAddress.getAllByName("192.168.10.3")[0] });
+ assertThat(enigma2DiscoveryParticipant.createResult(requireNonNull(serviceInfo)), is(nullValue()));
+ }
+
+ @Test
+ public void testGetThingUID() throws Exception {
+ when(serviceInfo.getInet4Addresses())
+ .thenReturn(new Inet4Address[] { (Inet4Address) InetAddress.getAllByName("192.168.10.3")[0] });
+ assertThat(enigma2DiscoveryParticipant.getThingUID(requireNonNull(serviceInfo)),
+ is(new ThingUID(Enigma2BindingConstants.THING_TYPE_DEVICE, "192_168_10_3")));
+ }
+
+ @Test
+ public void testGetThingUIDTwoAddresses() throws Exception {
+ when(serviceInfo.getName()).thenReturn("enigma2");
+ Inet4Address[] addresses = { (Inet4Address) InetAddress.getAllByName("192.168.10.3")[0],
+ (Inet4Address) InetAddress.getAllByName("192.168.10.4")[0] };
+ when(serviceInfo.getInet4Addresses()).thenReturn(addresses);
+ assertThat(enigma2DiscoveryParticipant.getThingUID(requireNonNull(serviceInfo)),
+ is(new ThingUID(Enigma2BindingConstants.THING_TYPE_DEVICE, "192_168_10_3")));
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index ff7aed0a0a344..15a93b0d8377c 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -85,6 +85,7 @@
org.openhab.binding.ecobee
org.openhab.binding.elerotransmitterstick
org.openhab.binding.energenie
+ org.openhab.binding.enigma2
org.openhab.binding.enocean
org.openhab.binding.enturno
org.openhab.binding.etherrain