From f485114366df402bd86d6b2c5a3e2b729d3f8bdb Mon Sep 17 00:00:00 2001 From: gdolfen <44291904+gdolfen@users.noreply.github.com> Date: Mon, 25 May 2020 07:16:49 +0200 Subject: [PATCH] [enigma2] Initial Contribution (#7514) * [enigma2] Initial contribution #7514 - Fixed review finding Signed-off-by: gdolfen --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.enigma2/NOTICE | 13 + bundles/org.openhab.binding.enigma2/README.md | 409 ++++++++++++++++++ bundles/org.openhab.binding.enigma2/pom.xml | 16 + .../src/main/feature/feature.xml | 9 + .../enigma2/actions/Enigma2Actions.java | 180 ++++++++ .../enigma2/actions/IEnigma2Actions.java | 41 ++ .../enigma2/handler/Enigma2Handler.java | 301 +++++++++++++ .../internal/Enigma2BindingConstants.java | 56 +++ .../enigma2/internal/Enigma2Client.java | 351 +++++++++++++++ .../internal/Enigma2Configuration.java | 45 ++ .../internal/Enigma2HandlerFactory.java | 52 +++ .../enigma2/internal/Enigma2HttpClient.java | 41 ++ .../enigma2/internal/Enigma2RemoteKey.java | 87 ++++ .../Enigma2DiscoveryParticipant.java | 111 +++++ .../resources/ESH-INF/binding/binding.xml | 10 + .../resources/ESH-INF/i18n/enigma2.properties | 36 ++ .../resources/ESH-INF/thing/thing-types.xml | 99 +++++ .../enigma2/actions/Enigma2ActionsTest.java | 184 ++++++++ .../enigma2/handler/Enigma2HandlerTest.java | 381 ++++++++++++++++ .../enigma2/internal/Enigma2ClientTest.java | 323 ++++++++++++++ .../internal/Enigma2HandlerFactoryTest.java | 67 +++ .../internal/Enigma2RemoteKeyTest.java | 32 ++ .../Enigma2DiscoveryParticipantTest.java | 114 +++++ bundles/pom.xml | 1 + 26 files changed, 2965 insertions(+) create mode 100644 bundles/org.openhab.binding.enigma2/NOTICE create mode 100644 bundles/org.openhab.binding.enigma2/README.md create mode 100644 bundles/org.openhab.binding.enigma2/pom.xml create mode 100644 bundles/org.openhab.binding.enigma2/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/actions/Enigma2Actions.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/actions/IEnigma2Actions.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/handler/Enigma2Handler.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2BindingConstants.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Client.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2Configuration.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2HandlerFactory.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2HttpClient.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/Enigma2RemoteKey.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/java/org/openhab/binding/enigma2/internal/discovery/Enigma2DiscoveryParticipant.java create mode 100644 bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/i18n/enigma2.properties create mode 100644 bundles/org.openhab.binding.enigma2/src/main/resources/ESH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/actions/Enigma2ActionsTest.java create mode 100644 bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/handler/Enigma2HandlerTest.java create mode 100644 bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2ClientTest.java create mode 100644 bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2HandlerFactoryTest.java create mode 100644 bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/Enigma2RemoteKeyTest.java create mode 100644 bundles/org.openhab.binding.enigma2/src/test/java/org/openhab/binding/enigma2/internal/discovery/Enigma2DiscoveryParticipantTest.java 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