From 67e6f8bf30c29f567b4634b9f2a8cb02ef7a008c Mon Sep 17 00:00:00 2001 From: Espen Fossen Date: Thu, 13 Jun 2024 22:33:14 +0200 Subject: [PATCH] [emotiva] Initial contribution (#16499) * [emotiva] Initial contribution Signed-off-by: Espen Fossen --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.emotiva/NOTICE | 13 + bundles/org.openhab.binding.emotiva/README.md | 190 +++++ bundles/org.openhab.binding.emotiva/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/EmotivaBindingConstants.java | 171 ++++ .../internal/EmotivaCommandHelper.java | 112 +++ .../internal/EmotivaConfiguration.java | 36 + .../internal/EmotivaHandlerFactory.java | 72 ++ .../internal/EmotivaProcessorHandler.java | 786 ++++++++++++++++++ .../internal/EmotivaTranslationProvider.java | 66 ++ .../internal/EmotivaUdpBroadcastService.java | 195 +++++ .../internal/EmotivaUdpReceivingService.java | 224 +++++ .../internal/EmotivaUdpSendingService.java | 213 +++++ .../internal/InputStateOptionProvider.java | 106 +++ .../discovery/EmotivaDiscoveryService.java | 68 ++ .../internal/dto/AbstractJAXBElementDTO.java | 50 ++ .../internal/dto/AbstractNotificationDTO.java | 42 + .../emotiva/internal/dto/ControlDTO.java | 80 ++ .../emotiva/internal/dto/EmotivaAckDTO.java | 51 ++ .../internal/dto/EmotivaBarNotifyDTO.java | 137 +++ .../internal/dto/EmotivaBarNotifyWrapper.java | 46 + .../internal/dto/EmotivaCommandDTO.java | 146 ++++ .../internal/dto/EmotivaControlDTO.java | 59 ++ .../emotiva/internal/dto/EmotivaMenuCol.java | 112 +++ .../internal/dto/EmotivaMenuNotifyDTO.java | 68 ++ .../internal/dto/EmotivaMenuProgress.java | 42 + .../emotiva/internal/dto/EmotivaMenuRow.java | 56 ++ .../internal/dto/EmotivaNotifyDTO.java | 90 ++ .../internal/dto/EmotivaNotifyWrapper.java | 47 ++ .../emotiva/internal/dto/EmotivaPingDTO.java | 39 + .../internal/dto/EmotivaPropertyDTO.java | 72 ++ .../internal/dto/EmotivaSubscriptionDTO.java | 50 ++ .../dto/EmotivaSubscriptionRequest.java | 63 ++ .../dto/EmotivaSubscriptionResponse.java | 52 ++ .../internal/dto/EmotivaTransponderDTO.java | 57 ++ .../internal/dto/EmotivaUnsubscribeDTO.java | 56 ++ .../internal/dto/EmotivaUpdateRequest.java | 66 ++ .../internal/dto/EmotivaUpdateResponse.java | 37 + .../internal/protocol/EmotivaCommandType.java | 40 + .../protocol/EmotivaControlCommands.java | 240 ++++++ .../protocol/EmotivaControlRequest.java | 475 +++++++++++ .../internal/protocol/EmotivaDataType.java | 56 ++ .../protocol/EmotivaPropertyStatus.java | 37 + .../protocol/EmotivaProtocolVersion.java | 46 + .../protocol/EmotivaSubscriptionTags.java | 186 +++++ .../internal/protocol/EmotivaUdpResponse.java | 34 + .../internal/protocol/EmotivaXmlUtils.java | 298 +++++++ .../protocol/OHChannelToEmotivaCommand.java | 115 +++ .../src/main/resources/OH-INF/addon/addon.xml | 31 + .../main/resources/OH-INF/config/config.xml | 61 ++ .../resources/OH-INF/i18n/emotiva.properties | 201 +++++ .../resources/OH-INF/thing/thing-types.xml | 532 ++++++++++++ .../emotiva/internal/AbstractDTOTestBase.java | 311 +++++++ .../internal/EmotivaCommandHelperTest.java | 106 +++ .../internal/dto/EmotivaAckDTOTest.java | 64 ++ .../internal/dto/EmotivaBarNotifyDTOTest.java | 51 ++ .../internal/dto/EmotivaCommandDTOTest.java | 75 ++ .../internal/dto/EmotivaControlDTOTest.java | 76 ++ .../dto/EmotivaMenuNotifyDTOTest.java | 65 ++ .../dto/EmotivaNotifyWrapperTest.java | 108 +++ .../internal/dto/EmotivaPingDTOTest.java | 67 ++ .../internal/dto/EmotivaPropertyDTOTest.java | 59 ++ .../dto/EmotivaSubscriptionRequestTest.java | 92 ++ .../dto/EmotivaSubscriptionResponseTest.java | 100 +++ .../dto/EmotivaTransponderDTOTest.java | 66 ++ .../dto/EmotivaUnsubscriptionTest.java | 65 ++ .../dto/EmotivaUpdateRequestTest.java | 58 ++ .../dto/EmotivaUpdateResponseTest.java | 97 +++ .../protocol/EmotivaControlRequestTest.java | 270 ++++++ .../protocol/EmotivaXmlUtilsTest.java | 75 ++ bundles/pom.xml | 1 + 73 files changed, 7960 insertions(+) create mode 100644 bundles/org.openhab.binding.emotiva/NOTICE create mode 100644 bundles/org.openhab.binding.emotiva/README.md create mode 100644 bundles/org.openhab.binding.emotiva/pom.xml create mode 100644 bundles/org.openhab.binding.emotiva/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaBindingConstants.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelper.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaConfiguration.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaHandlerFactory.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorHandler.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaTranslationProvider.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpBroadcastService.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpReceivingService.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpSendingService.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/InputStateOptionProvider.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/discovery/EmotivaDiscoveryService.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractJAXBElementDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractNotificationDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/ControlDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyWrapper.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuCol.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuProgress.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuRow.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapper.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponse.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscribeDTO.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponse.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaCommandType.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlCommands.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaDataType.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaPropertyStatus.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaProtocolVersion.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaSubscriptionTags.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaUdpResponse.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtils.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/OHChannelToEmotivaCommand.java create mode 100644 bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/i18n/emotiva.properties create mode 100644 bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/AbstractDTOTestBase.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelperTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTOTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTOTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTOTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTOTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTOTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapperTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTOTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTOTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequestTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponseTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTOTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscriptionTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequestTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponseTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequestTest.java create mode 100644 bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtilsTest.java diff --git a/CODEOWNERS b/CODEOWNERS index fbe2c959274..49fc1684d31 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -96,6 +96,7 @@ /bundles/org.openhab.binding.electroluxair/ @jannegpriv /bundles/org.openhab.binding.elerotransmitterstick/ @vbier /bundles/org.openhab.binding.elroconnects/ @mherwege +/bundles/org.openhab.binding.emotiva/ @espenaf /bundles/org.openhab.binding.energenie/ @hmerk /bundles/org.openhab.binding.energidataservice/ @jlaur /bundles/org.openhab.binding.enigma2/ @gdolfen diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 86fba926572..f97b4b7010c 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -471,6 +471,11 @@ org.openhab.binding.elroconnects ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.emotiva + ${project.version} + org.openhab.addons.bundles org.openhab.binding.energenie diff --git a/bundles/org.openhab.binding.emotiva/NOTICE b/bundles/org.openhab.binding.emotiva/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/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.emotiva/README.md b/bundles/org.openhab.binding.emotiva/README.md new file mode 100644 index 00000000000..1008c660ec4 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/README.md @@ -0,0 +1,190 @@ +# Emotiva Binding + +This binding integrates Emotiva AV processors by using the Emotiva Network Remote Control protocol. + +## Supported Things + +This binding supports Emotiva processors with Emotiva Network Remote Control protocol support. +The thing type for all of them is `processor`. + +Tested models: Emotiva XMC-2 + +## Discovery + +The binding automatically discovers devices on your network. + +## Thing Configuration + +The Emotiva Processor thing requires the `ipAddress` it can connect to. +There are more parameters which all have defaults set. + +| Parameter | Values | Default | +|-----------------------|---------------------------------------------------------------|---------| +| ipAddress | IP address of the processor | - | +| controlPort | port number, e.g. 7002 | 7002 | +| notifyPort | port number, e.g. 7003 | 7003 | +| infoPort | port number, e.g. 7004 | 7004 | +| setupPortTCP | port number, e.g. 7100 | 7100 | +| menuNotifyPort | port number, e.g. 7005 | 7005 | +| protocolVersion | Emotiva Network Protocol version, e.g. 3.0 | 2.0 | +| keepAlive | Time between notification update from device, in milliseconds | 7500 | +| retryConnectInMinutes | Time between connection retry, in minutes | 2 | + + +## Channels + +The Emotiva Processor supports the following channels (some channels are model specific): + +| Channel Type ID | Item Type | Description | +|------------------------------------|--------------------|------------------------------------------------------------| +| _Main zone_ | | | +| main-zone#power | Switch (RW) | Main zone power on/off | +| main-zone#volume | Dimmer (RW) | Main zone volume in percentage (0 to 100) | +| main-zone#volume-db | Number (RW) | Main zone volume in dB (-96 to 15) | +| main-zone#mute | Switch (RW) | Main zone mute | +| main-zone#source | String (RW) | Main zone input (HDMI1, TUNER, ARC, ...) | +| _Zone 2_ | | | +| zone2#power | Switch (RW) | Zone 2 power on/off | +| zone2#volume | Dimmer (RW) | Zone 2 volume in percentage (0 to 100) | +| zone2#volume-db | Number (RW) | Zone 2 volume in dB (-80 offset) | +| zone2#mute | Switch (RW) | Zone 2 mute | +| zone2#input | String (RW) | Zone 2 input | +| _General_ | | | +| general#power | Switch (RW) | Power on/off | +| general#standby | String (W) | Set in standby mode | +| general#menu | String (RW) | Enter or exit menu | +| general#menu-control | String (W) | Control menu via string commands | +| general#up | String (W) | Menu up | +| general#down | String (W) | Menu down | +| general#left | String (W) | Menu left | +| general#right | String (W) | Menu right | +| general#enter | String (W) | Menu enter | +| general#dim | Switch (RW) | Cycle through FP dimness settings | +| general#mode | String (RW) | Select audio mode (auto, dts, ...) | +| general#info | String (W) | Show info screen | +| general#speaker-preset | String (RW) | Select speaker presets (preset1, preset2) | +| general#center | Number (RW) | Center Volume increment up/down (0.5 step) | +| general#subwoofer | Number (RW) | Subwoofer Volume increment up/down (0.5 step) | +| general#surround | Number (RW) | Surround Volume increment up/down (0.5 step) | +| general#back | Number (RW) | Back Volume increment up/down (0.5 step) | +| general#loudness | Switch (RW) | Loudness on/off | +| general#treble | Number (RW) | Treble Volume increment up/down (0.5 step) | +| general#bass | Number (RW) | Bass Volume increment up/down (0.5 step) | +| general#frequenncy | Rollershutter (W) | Frequency up/down, (100 kHz step) | +| general#seek | Rollershutter (W) | Seek signal up/down | +| general#channel | Rollershutter (W) | Channel up/down | +| general#tuner-band | String (R) | Tuner band, (AM, FM) | +| general#tuner-channel | String (RW) | User–assigned station name | +| general#tuner-signal | String (R) | Tuner signal quality | +| general#tuner-program | String (R) | Tuner program: "Country", "Rock", ... | +| general#tuner-RDS | String (R) | Tuner RDS string | +| general#audio-input | String (R) | Audio input source | +| general#audio-bitstream | String (R) | Audio input bitstream type: "PCM 2.0", "ATMOS", etc. | +| general#audio-bits | String (R) | Audio input bits: "32kHZ 24bits", etc. | +| general#video-input | String (R) | Video input source | +| general#video-format | String (R) | Video input format: "1920x1080i/60", "3840x2160p/60", etc. | +| general#video-space | String (R) | Video input space: "YcbCr 8bits", etc. | +| general#input-[1-8] | String (R) | User assigned input names | +| general#selected-mode | String (R) | User selected mode for the main zone | +| general#selected-movie-music | String (R) | User selected movie or music mode for main zone | +| general#mode-ref-stereo | String (R) | Label for mode: Reference Stereo | +| general#mode-stereo | String (R) | Label for mode: Stereo | +| general#mode-music | String (R) | Label for mode: Music | +| general#mode-movie | String (R) | Label for mode: Movie | +| general#mode-direct | String (R) | Label for mode: Direct | +| general#mode-dolby | String (R) | Label for mode: Dolby | +| general#mode-dts | String (R) | Label for mode: DTS | +| general#mode-all-stereo | String (R) | Label for mode: All Stereo | +| general#mode-auto | String (R) | Label for mode: Auto | +| general#mode-surround | String (RW) | Select audio mode (Auto, Stereo, Dolby, ...) | +| general#width | Number (RW) | Width Volume increment up/down (0.5 step) | +| general#height | Number (RW) | Height Volume increment up/down (0.5 step) | +| general#bar | String (R) | Text displayed on front panel bar of device | +| general#menu-display-highlight | String (R) | Menu Panel Display: Value in focus | +| general#menu-display-top-start | String (R) | Menu Panel Display: Top bar, start cell | +| general#menu-display-top-center | String (R) | Menu Panel Display: Top bar, center cell | +| general#menu-display-top-end | String (R) | Menu Panel Display: Top bar, end cell | +| general#menu-display-middle-start | String (R) | Menu Panel Display: Middle bar, start cell | +| general#menu-display-middle-center | String (R) | Menu Panel Display: Middle bar, center cell | +| general#menu-display-middle-end | String (R) | Menu Panel Display: Middle bar, end cell | +| general#menu-display-bottom-start | String (R) | Menu Panel Display: Bottom bar, start cell | +| general#menu-display-bottom-center | String (R) | Menu Panel Display: Bottom bar, center cell | +| general#menu-display-bottom-end | String (R) | Menu Panel Display: Bottom bar, end cell | + +(R) = read-only (no updates possible) +(W) = write-only +(RW) = read-write + +## Full Example + +### `.things` file: + +```perl +Thing emotiva:processor:1 "XMC-2" @ "Living room" [ipAddress="10.0.0.100", protocolVersion="3.0"] +``` + +### `.items` file: + +```perl +Switch emotiva-power "Processor" {channel="emotiva:processor:1:general#power"} +Dimmer emotiva-volume "Volume [%d %%]" {channel="emotiva:processor:1:main-zone#volume"} +Number:Dimensionless emotiva-volume-db "Volume [%d dB]" {channel="emotiva:processor:1:main-zone#volume-db"} +Switch emotiva-mute "Mute" {channel="emotiva:processor:1:main-zone#mute"} +String emotiva-source "Source [%s]" {channel="emotiva:processor:1:main-zone#input"} +String emotiva-mode-surround "Surround Mode: [%s]" {channel="emotiva:processor:1:general#mode-surround"} +Number:Dimensionless emotiva-speakers-center "Center Trim [%.1f dB]" {channel="emotiva:processor:1:general#center"} +Switch emotiva-zone2power "Zone 2" {channel="emotiva:processor:1:zone2#power"} +String emotiva-front-panel-bar "Bar Text" {channel="emotiva:processor:1:general#bar"} +String emotiva-menu-control "Menu Control" {channel="emotiva:processor:1:general#menu-control"} +String emotiva-menu-hightlight "Menu field focus" {channel="emotiva:processor:1:general#menu-display-highlight"} +String emotiva-menu-top-start "" {channel="emotiva:processor:1:general#menu-display-top-start"} +String emotiva-menu-top-center "" {channel="emotiva:processor:1:general#menu-display-top-center"} +String emotiva-menu-top-end "" {channel="emotiva:processor:1:general#menu-display-top-end"} +String emotiva-menu-middle-start "" {channel="emotiva:processor:1:general#menu-display-middle-start"} +String emotiva-menu-middle-center "" {channel="emotiva:processor:1:general#menu-display-middle-center"} +String emotiva-menu-middle-end "" {channel="emotiva:processor:1:general#menu-display-middle-end"} +String emotiva-menu-tottom-start "" {channel="emotiva:processor:1:general#menu-display-bottom-start"} +String emotiva-menu-tottom-center "" {channel="emotiva:processor:1:general#menu-display-bottom-center"} +String emotiva-menu-tottom-end "" {channel="emotiva:processor:1:general#menu-display-bottom-end"} +``` + +### `.sitemap` file: + +```perl +Group item=emotiva-input label="Processor" icon="receiver" { + Default item=emotiva-power + Default item=emotiva-mute + Setpoint item=emotiva-volume + Default item=emotiva-volume-db step=2 minValue=-96.0 maxValue=15.0 + Selection item=emotiva-source + Text item=emotiva-mode-surround + Setpoint item=emotiva-speakers-center step=0.5 minValue=-12.0 maxValue=12.0 + Default item=emotiva-zone2power +} +Frame label="Front Panel" { + Text item=emotiva-front-panel-bar + Text item=emotiva-menu-highlight + Frame label="" { + Text item=emotiva-menu-top-start + Text item=emotiva-menu-top-center + Text item=emotiva-menu-top-end + } + Frame label="" { + Text item=emotiva-menu-middle-start + Text item=emotiva-menu-middle-center + Text item=emotiva-menu-middle-end + } + Frame label="" { + Text item=emotiva-menu-bottom-start + Text item=emotiva-menu-bottom-center + Text item=emotiva-menu-bottom-end + } + Buttongrid label="Menu Control" staticIcon=material:control-camera item=emotiva-menu_control buttons=[1:1:POWER="Power"=switch-off , 1:2:MENU="Menu", 1:3:INFO="Info" , 2:2:UP="Up"=f7:arrowtriangle_up , 4:2:DOWN="Down"=f7:arrowtriangle_down , 3:1:LEFT="Left"=f7:arrowtriangle_left , 3:3:RIGHT="Right"=f7:arrowtriangle_right , 3:2:ENTER="Select" ] +} +``` + +## Network Remote Control Protocol Reference + +These resources can be useful to learn what to send using the `command` channel: + +- [Emotiva Remote Interface Description](https://www.dropbox.com/sh/lvo9lbhu89jqfdb/AACa4iguvWK3I6ONjIpyM5Zca/Emotiva_Remote_Interface_Description%20V3.1.docx) diff --git a/bundles/org.openhab.binding.emotiva/pom.xml b/bundles/org.openhab.binding.emotiva/pom.xml new file mode 100644 index 00000000000..9cdd6814886 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.2.0-SNAPSHOT + + + org.openhab.binding.emotiva + + openHAB Add-ons :: Bundles :: Emotiva Binding + + diff --git a/bundles/org.openhab.binding.emotiva/src/main/feature/feature.xml b/bundles/org.openhab.binding.emotiva/src/main/feature/feature.xml new file mode 100644 index 00000000000..df087808cb3 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/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.emotiva/${project.version} + + diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaBindingConstants.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaBindingConstants.java new file mode 100644 index 00000000000..33d9fd0596e --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaBindingConstants.java @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link EmotivaBindingConstants} class defines common constants, which are used across the whole binding. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class EmotivaBindingConstants { + + public static final String BINDING_ID = "emotiva"; + + /** Property name to uniquely identify (discovered) things. */ + static final String UNIQUE_PROPERTY_NAME = "ip4Address"; + + /** Default port used to discover Emotiva devices. */ + static final int DEFAULT_PING_PORT = 7000; + + /** Default port used to receive transponder (discovered) Emotiva devices. */ + static final int DEFAULT_TRANSPONDER_PORT = 7001; + + /** Default timeout in milliseconds for sending UDP packets. */ + static final int DEFAULT_UDP_SENDING_TIMEOUT = 1000; + + /** Number of connection attempts, set OFFLINE if no success and a retry job is then started. */ + static final int DEFAULT_CONNECTION_RETRIES = 3; + + /** Connection retry interval in minutes */ + static final int DEFAULT_RETRY_INTERVAL_MINUTES = 2; + + /** + * Default Emotiva device keep alive in milliseconds. {@link org.openhab.binding.emotiva.internal.dto.ControlDTO} + */ + static final int DEFAULT_KEEP_ALIVE_IN_MILLISECONDS = 7500; + + /** State name for storing keepAlive timestamp messages */ + public static final String LAST_SEEN_STATE_NAME = "no-channel#last-seen"; + + /** + * Default Emotiva device considered list in milliseconds. + * {@link org.openhab.binding.emotiva.internal.dto.ControlDTO} + */ + static final int DEFAULT_KEEP_ALIVE_CONSIDERED_LOST_IN_MILLISECONDS = 30000; + + /** Default Emotiva control message value **/ + public static final String DEFAULT_CONTROL_MESSAGE_SET_DEFAULT_VALUE = "0"; + + /** Default value for ack property in Emotiva control messages **/ + public static final String DEFAULT_CONTROL_ACK_VALUE = "yes"; + + /** Default discovery timeout in seconds **/ + public static final int DISCOVERY_TIMEOUT_SECONDS = 5; + + /** Default discovery broadcast address **/ + public static final String DISCOVERY_BROADCAST_ADDRESS = "255.255.255.255"; + + /** List of all Thing Type UIDs **/ + static final ThingTypeUID THING_PROCESSOR = new ThingTypeUID(BINDING_ID, "processor"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet<>(List.of(THING_PROCESSOR)); + + /** Default values for Emotiva channels **/ + public static final String DEFAULT_EMOTIVA_PROTOCOL_VERSION = "2.0"; + public static final int DEFAULT_VOLUME_MIN_DECIBEL = -96; + public static final int DEFAULT_VOLUME_MAX_DECIBEL = 15; + public static final int DEFAULT_TRIM_MIN_DECIBEL = -12; + public static final int DEFAULT_TRIM_MAX_DECIBEL = 12; + public static final String MAP_SOURCES_MAIN_ZONE = "sources"; + public static final String MAP_SOURCES_ZONE_2 = "zone2-sources"; + + /** Miscellaneous Constants **/ + public static final int PROTOCOL_V3_LEVEL_MULTIPLIER = 2; + public static final String TRIM_SET_COMMAND_SUFFIX = "_trim_set"; + static final String MENU_PANEL_CHECKBOX_ON = "on"; + static final String MENU_PANEL_HIGHLIGHTED = "true"; + static final String EMOTIVA_SOURCE_COMMAND_PREFIX = "source_"; + + /** Emotiva Protocol V1 channels **/ + public static final String CHANNEL_STANDBY = "general#standby"; + public static final String CHANNEL_MAIN_ZONE_POWER = "main-zone#power"; + public static final String CHANNEL_SOURCE = "main-zone#source"; + public static final String CHANNEL_MENU = "general#menu"; + public static final String CHANNEL_MENU_CONTROL = "general#menu-control"; + public static final String CHANNEL_MENU_UP = "general#up"; + public static final String CHANNEL_MENU_DOWN = "general#down"; + public static final String CHANNEL_MENU_LEFT = "general#left"; + public static final String CHANNEL_MENU_RIGHT = "general#right"; + public static final String CHANNEL_MENU_ENTER = "general#enter"; + public static final String CHANNEL_MUTE = "main-zone#mute"; + public static final String CHANNEL_DIM = "general#dim"; + public static final String CHANNEL_MODE = "general#mode"; + public static final String CHANNEL_CENTER = "general#center"; + public static final String CHANNEL_SUBWOOFER = "general#subwoofer"; + public static final String CHANNEL_SURROUND = "general#surround"; + public static final String CHANNEL_BACK = "general#back"; + public static final String CHANNEL_MODE_SURROUND = "general#mode-surround"; + public static final String CHANNEL_SPEAKER_PRESET = "general#speaker-preset"; + public static final String CHANNEL_MAIN_VOLUME = "main-zone#volume"; + public static final String CHANNEL_MAIN_VOLUME_DB = "main-zone#volume_db"; + public static final String CHANNEL_LOUDNESS = "general#loudness"; + public static final String CHANNEL_ZONE2_POWER = "zone2#power"; + public static final String CHANNEL_ZONE2_VOLUME = "zone2#volume"; + public static final String CHANNEL_ZONE2_VOLUME_DB = "zone2#volume-db"; + public static final String CHANNEL_ZONE2_MUTE = "zone2#mute"; + public static final String CHANNEL_ZONE2_SOURCE = "zone2#source"; + public static final String CHANNEL_FREQUENCY = "general#frequency"; + public static final String CHANNEL_SEEK = "general#seek"; + public static final String CHANNEL_CHANNEL = "general#channel"; + public static final String CHANNEL_TUNER_BAND = "general#tuner-band"; + public static final String CHANNEL_TUNER_CHANNEL = "general#tuner-channel"; + public static final String CHANNEL_TUNER_CHANNEL_SELECT = "general#tuner-channel-select"; + public static final String CHANNEL_TUNER_SIGNAL = "general#tuner-signal"; + public static final String CHANNEL_TUNER_PROGRAM = "general#tuner-program"; + public static final String CHANNEL_TUNER_RDS = "general#tuner-RDS"; + public static final String CHANNEL_AUDIO_INPUT = "general#audio-input"; + public static final String CHANNEL_AUDIO_BITSTREAM = "general#audio-bitstream"; + public static final String CHANNEL_AUDIO_BITS = "general#audio-bits"; + public static final String CHANNEL_VIDEO_INPUT = "general#video-input"; + public static final String CHANNEL_VIDEO_FORMAT = "general#video-format"; + public static final String CHANNEL_VIDEO_SPACE = "general#video-space"; + public static final String CHANNEL_INPUT1 = "general#input-1"; + public static final String CHANNEL_INPUT2 = "general#input-2"; + public static final String CHANNEL_INPUT3 = "general#input-3"; + public static final String CHANNEL_INPUT4 = "general#input-4"; + public static final String CHANNEL_INPUT5 = "general#input-5"; + public static final String CHANNEL_INPUT6 = "general#input-6"; + public static final String CHANNEL_INPUT7 = "general#input-7"; + public static final String CHANNEL_INPUT8 = "general#input-8"; + public static final String CHANNEL_MODE_REF_STEREO = "general#mode-ref-stereo"; + public static final String CHANNEL_SURROUND_MODE = "general#surround-mode"; + public static final String CHANNEL_MODE_STEREO = "general#mode-stereo"; + public static final String CHANNEL_MODE_MUSIC = "general#mode-music"; + public static final String CHANNEL_MODE_MOVIE = "general#mode-movie"; + public static final String CHANNEL_MODE_DIRECT = "general#mode-direct"; + public static final String CHANNEL_MODE_DOLBY = "general#mode-dolby"; + public static final String CHANNEL_MODE_DTS = "general#mode-dts"; + public static final String CHANNEL_MODE_ALL_STEREO = "general#mode-all-stereo"; + public static final String CHANNEL_MODE_AUTO = "general#mode-auto"; + + /** Emotiva Protocol V2 channels **/ + public static final String CHANNEL_SELECTED_MODE = "general#selected-mode"; + public static final String CHANNEL_SELECTED_MOVIE_MUSIC = "general#selected-movie-music"; + + /** Emotiva Protocol V3 channels **/ + public static final String CHANNEL_TREBLE = "general#treble"; + public static final String CHANNEL_BASS = "general#bass"; + public static final String CHANNEL_WIDTH = "general#width"; + public static final String CHANNEL_HEIGHT = "general#height"; + public static final String CHANNEL_BAR = "general#bar"; + public static final String CHANNEL_MENU_DISPLAY_PREFIX = "general#menu-display"; + public static final String CHANNEL_MENU_DISPLAY_HIGHLIGHT = "general#menu-display-highlight"; +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelper.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelper.java new file mode 100644 index 00000000000..ec79969e853 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelper.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlRequest; +import org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; +import org.openhab.binding.emotiva.internal.protocol.OHChannelToEmotivaCommand; +import org.openhab.core.library.types.PercentType; + +/** + * Helper class for Emotiva commands. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class EmotivaCommandHelper { + + public static PercentType volumeDecibelToPercentage(String volumeInDecibel) { + String volumeTrimmed = volumeInDecibel.replace("dB", "").trim(); + int clampedValue = clamp(volumeTrimmed, DEFAULT_VOLUME_MIN_DECIBEL, DEFAULT_VOLUME_MAX_DECIBEL); + return new PercentType(Math.round((100 - ((float) Math.abs(clampedValue - DEFAULT_VOLUME_MAX_DECIBEL) + / Math.abs(DEFAULT_VOLUME_MIN_DECIBEL - DEFAULT_VOLUME_MAX_DECIBEL)) * 100))); + } + + public static double integerToPercentage(int integer) { + int clampedValue = clamp(integer, 0, 100); + return Math.round((100 - ((float) Math.abs(clampedValue - 100) / Math.abs(-100)) * 100)); + } + + public static int volumePercentageToDecibel(int volumeInPercentage) { + int clampedValue = clamp(volumeInPercentage, 0, 100); + return (clampedValue * (DEFAULT_VOLUME_MAX_DECIBEL - DEFAULT_VOLUME_MIN_DECIBEL) / 100) + + DEFAULT_VOLUME_MIN_DECIBEL; + } + + public static int volumePercentageToDecibel(String volumeInPercentage) { + String volumeInPercentageTrimmed = volumeInPercentage.replace("%", "").trim(); + int clampedValue = clamp(volumeInPercentageTrimmed, 0, 100); + return (clampedValue * (DEFAULT_VOLUME_MAX_DECIBEL - DEFAULT_VOLUME_MIN_DECIBEL) / 100) + + DEFAULT_VOLUME_MIN_DECIBEL; + } + + public static double clamp(Number value, double min, double max) { + return Math.min(Math.max(value.intValue(), min), max); + } + + private static int clamp(String volumeInPercentage, int min, int max) { + return Math.min(Math.max(Double.valueOf(volumeInPercentage.trim()).intValue(), min), max); + } + + private static int clamp(int volumeInPercentage, int min, int max) { + return Math.min(Math.max(Double.valueOf(volumeInPercentage).intValue(), min), max); + } + + public static EmotivaControlRequest channelToControlRequest(String id, + Map> commandMaps, EmotivaProtocolVersion protocolVersion) { + EmotivaSubscriptionTags channelSubscription = EmotivaSubscriptionTags.fromChannelUID(id); + EmotivaControlCommands channelFromCommand = OHChannelToEmotivaCommand.fromChannelUID(id); + return new EmotivaControlRequest(id, channelSubscription, channelFromCommand, commandMaps, protocolVersion); + } + + public static String getMenuPanelRowLabel(int row) { + return switch (row) { + case 4 -> "top"; + case 5 -> "middle"; + case 6 -> "bottom"; + default -> ""; + }; + } + + public static String getMenuPanelColumnLabel(int column) { + return switch (column) { + case 0 -> "start"; + case 1 -> "center"; + case 2 -> "end"; + default -> ""; + }; + } + + public static String updateProgress(double progressPercentage) { + final int width = 30; + StringBuilder sb = new StringBuilder(); + + sb.append("["); + int i = 0; + for (; i <= (int) (progressPercentage * width); i++) { + sb.append("."); + } + for (; i < width; i++) { + sb.append(" "); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaConfiguration.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaConfiguration.java new file mode 100644 index 00000000000..0d54d35f28e --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EmotivaConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class EmotivaConfiguration { + + public String ipAddress = ""; + public int controlPort = 7002; + public int notifyPort = 7003; + public int infoPort = 7004; + public int setupPortTCP = 7100; + public int menuNotifyPort = 7005; + public String protocolVersion = DEFAULT_EMOTIVA_PROTOCOL_VERSION; + public int keepAlive = DEFAULT_KEEP_ALIVE_IN_MILLISECONDS; + public int retryConnectInMinutes = DEFAULT_RETRY_INTERVAL_MINUTES; +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaHandlerFactory.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaHandlerFactory.java new file mode 100644 index 00000000000..52f9e1d0e0e --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaHandlerFactory.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.THING_PROCESSOR; + +import java.util.Set; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link org.openhab.core.thing.binding.ThingHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.emotiva", service = ThingHandlerFactory.class) +public class EmotivaHandlerFactory extends BaseThingHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(EmotivaHandlerFactory.class); + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_PROCESSOR); + private final EmotivaTranslationProvider i18nProvider; + + @Activate + public EmotivaHandlerFactory(final @Reference EmotivaTranslationProvider i18nProvider) { + this.i18nProvider = i18nProvider; + } + + @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_PROCESSOR.equals(thingTypeUID)) { + try { + return new EmotivaProcessorHandler(thing, i18nProvider); + } catch (JAXBException e) { + logger.debug("Could not create Emotiva Process Handler", e); + } + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorHandler.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorHandler.java new file mode 100644 index 00000000000..9dceae4aecb --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorHandler.java @@ -0,0 +1,786 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import static java.lang.String.format; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.channelToControlRequest; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.getMenuPanelColumnLabel; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.getMenuPanelRowLabel; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.updateProgress; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumeDecibelToPercentage; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumePercentageToDecibel; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.band_am; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.band_fm; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.channel_1; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.none; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.power_on; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.STRING; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus.NOT_VALID; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.protocolFromConfig; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.noSubscriptionToChannel; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_band; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_channel; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.quantity.Frequency; +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.emotiva.internal.dto.AbstractNotificationDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaAckDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaBarNotifyDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaBarNotifyWrapper; +import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaMenuNotifyDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyWrapper; +import org.openhab.binding.emotiva.internal.dto.EmotivaPropertyDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionResponse; +import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateResponse; +import org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlRequest; +import org.openhab.binding.emotiva.internal.protocol.EmotivaDataType; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; +import org.openhab.binding.emotiva.internal.protocol.EmotivaUdpResponse; +import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils; +import org.openhab.core.common.NamedThreadFactory; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The EmotivaProcessorHandler is responsible for handling OpenHAB commands, which are + * sent to one of the channels. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class EmotivaProcessorHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(EmotivaProcessorHandler.class); + + private final Map stateMap = Collections.synchronizedMap(new HashMap<>()); + + private final EmotivaConfiguration config; + + /** + * Emotiva devices have trouble with too many subscriptions in same request, so subscriptions are dividing into + * those general group channels, and the rest. + */ + private final EmotivaSubscriptionTags[] generalSubscription = EmotivaSubscriptionTags.generalChannels(); + private final EmotivaSubscriptionTags[] nonGeneralSubscriptions = EmotivaSubscriptionTags.nonGeneralChannels(); + + private final EnumMap sourcesMainZone; + private final EnumMap sourcesZone2; + private final EnumMap modes; + private final Map> commandMaps = new ConcurrentHashMap<>(); + private final EmotivaTranslationProvider i18nProvider; + + private @Nullable ScheduledFuture pollingJob; + private @Nullable ScheduledFuture connectRetryJob; + private @Nullable EmotivaUdpSendingService sendingService; + private @Nullable EmotivaUdpReceivingService notifyListener; + private @Nullable EmotivaUdpReceivingService menuNotifyListener; + + private final int retryConnectInMinutes; + + /** + * Thread factory for menu progress bar + */ + private final NamedThreadFactory listeningThreadFactory = new NamedThreadFactory(BINDING_ID, true); + + private final EmotivaXmlUtils xmlUtils = new EmotivaXmlUtils(); + + private boolean udpSenderActive = false; + + public EmotivaProcessorHandler(Thing thing, EmotivaTranslationProvider i18nProvider) throws JAXBException { + super(thing); + this.i18nProvider = i18nProvider; + this.config = getConfigAs(EmotivaConfiguration.class); + this.retryConnectInMinutes = config.retryConnectInMinutes; + + sourcesMainZone = new EnumMap<>(EmotivaControlCommands.class); + commandMaps.put(MAP_SOURCES_MAIN_ZONE, sourcesMainZone); + + sourcesZone2 = new EnumMap<>(EmotivaControlCommands.class); + commandMaps.put(MAP_SOURCES_ZONE_2, sourcesZone2); + + EnumMap channels = new EnumMap<>( + Map.ofEntries(Map.entry(channel_1, channel_1.getLabel()), + Map.entry(EmotivaControlCommands.channel_2, EmotivaControlCommands.channel_2.getLabel()), + Map.entry(EmotivaControlCommands.channel_3, EmotivaControlCommands.channel_3.getLabel()), + Map.entry(EmotivaControlCommands.channel_4, EmotivaControlCommands.channel_4.getLabel()), + Map.entry(EmotivaControlCommands.channel_5, EmotivaControlCommands.channel_5.getLabel()), + Map.entry(EmotivaControlCommands.channel_6, EmotivaControlCommands.channel_6.getLabel()), + Map.entry(EmotivaControlCommands.channel_7, EmotivaControlCommands.channel_7.getLabel()), + Map.entry(EmotivaControlCommands.channel_8, EmotivaControlCommands.channel_8.getLabel()), + Map.entry(EmotivaControlCommands.channel_9, EmotivaControlCommands.channel_9.getLabel()), + Map.entry(EmotivaControlCommands.channel_10, EmotivaControlCommands.channel_10.getLabel()), + Map.entry(EmotivaControlCommands.channel_11, EmotivaControlCommands.channel_11.getLabel()), + Map.entry(EmotivaControlCommands.channel_12, EmotivaControlCommands.channel_12.getLabel()), + Map.entry(EmotivaControlCommands.channel_13, EmotivaControlCommands.channel_13.getLabel()), + Map.entry(EmotivaControlCommands.channel_14, EmotivaControlCommands.channel_14.getLabel()), + Map.entry(EmotivaControlCommands.channel_15, EmotivaControlCommands.channel_15.getLabel()), + Map.entry(EmotivaControlCommands.channel_16, EmotivaControlCommands.channel_16.getLabel()), + Map.entry(EmotivaControlCommands.channel_17, EmotivaControlCommands.channel_17.getLabel()), + Map.entry(EmotivaControlCommands.channel_18, EmotivaControlCommands.channel_18.getLabel()), + Map.entry(EmotivaControlCommands.channel_19, EmotivaControlCommands.channel_19.getLabel()), + Map.entry(EmotivaControlCommands.channel_20, EmotivaControlCommands.channel_20.getLabel()))); + commandMaps.put(tuner_channel.getEmotivaName(), channels); + + EnumMap bands = new EnumMap<>( + Map.of(band_am, band_am.getLabel(), band_fm, band_fm.getLabel())); + commandMaps.put(tuner_band.getEmotivaName(), bands); + + modes = new EnumMap<>(EmotivaSubscriptionTags.class); + } + + @Override + public void initialize() { + logger.debug("Initialize: '{}'", getThing().getUID()); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/message.processor.connecting"); + if (config.controlPort < 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/message.processor.connection.error.port"); + return; + } + if (config.ipAddress.trim().isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/message.processor.connection.error.address-empty"); + return; + } else { + try { + // noinspection ResultOfMethodCallIgnored + InetAddress.getByName(config.ipAddress); + } catch (UnknownHostException ignored) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/message.processor.connection.error.address-invalid"); + return; + } + } + + scheduler.execute(this::connect); + } + + private synchronized void connect() { + final EmotivaConfiguration localConfig = config; + try { + final EmotivaUdpReceivingService notifyListener = new EmotivaUdpReceivingService(localConfig.notifyPort, + localConfig, scheduler); + this.notifyListener = notifyListener; + notifyListener.connect(this::handleStatusUpdate, true); + + final EmotivaUdpSendingService sendConnector = new EmotivaUdpSendingService(localConfig, scheduler); + sendingService = sendConnector; + sendConnector.connect(this::handleStatusUpdate, true); + + // Simple retry mechanism to handle minor network issues, if this fails a retry job is created + for (int attempt = 1; attempt <= DEFAULT_CONNECTION_RETRIES && !udpSenderActive; attempt++) { + try { + logger.debug("Connection attempt '{}'", attempt); + sendConnector.sendSubscription(generalSubscription, config); + sendConnector.sendSubscription(nonGeneralSubscriptions, config); + } catch (IOException e) { + // network or socket failure, also wait 2 sec and try again + } + + for (int delay = 0; delay < 10 && !udpSenderActive; delay++) { + Thread.sleep(200); // wait 10 x 200ms = 2sec + } + } + + if (udpSenderActive) { + updateStatus(ThingStatus.ONLINE); + + final EmotivaUdpReceivingService menuListenerConnector = new EmotivaUdpReceivingService( + localConfig.menuNotifyPort, localConfig, scheduler); + this.menuNotifyListener = menuListenerConnector; + menuListenerConnector.connect(this::handleStatusUpdate, true); + + startPollingKeepAlive(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "@text/message.processor.connection.failed"); + disconnect(); + scheduleConnectRetry(retryConnectInMinutes); + } + } catch (InterruptedException e) { + // OH shutdown - don't log anything, Framework will call dispose() + } catch (Exception e) { + logger.error("Connection to '{}' failed", localConfig.ipAddress, e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "@text/message.processor.connection.failed"); + disconnect(); + scheduleConnectRetry(retryConnectInMinutes); + } + } + + private void scheduleConnectRetry(long waitMinutes) { + logger.debug("Scheduling connection retry in '{}' minutes", waitMinutes); + connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES); + } + + /** + * Starts a polling job for connection to th device, adds the + * {@link EmotivaBindingConstants#DEFAULT_KEEP_ALIVE_IN_MILLISECONDS} as a time buffer for checking, to avoid + * flapping state or minor network issues. + */ + private void startPollingKeepAlive() { + final ScheduledFuture localRefreshJob = this.pollingJob; + if (localRefreshJob == null || localRefreshJob.isCancelled()) { + logger.debug("Start polling"); + + int delay = stateMap.get(EmotivaSubscriptionTags.keepAlive.name()) != null + && stateMap.get(EmotivaSubscriptionTags.keepAlive.name()) instanceof Number keepAlive + ? keepAlive.intValue() + : config.keepAlive; + pollingJob = scheduler.scheduleWithFixedDelay(this::checkKeepAliveTimestamp, + delay + DEFAULT_KEEP_ALIVE_IN_MILLISECONDS, delay + DEFAULT_KEEP_ALIVE_IN_MILLISECONDS, + TimeUnit.MILLISECONDS); + } + } + + private void checkKeepAliveTimestamp() { + if (ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) { + State state = stateMap.get(LAST_SEEN_STATE_NAME); + if (state instanceof Number value) { + Instant lastKeepAliveMessageTimestamp = Instant.ofEpochSecond(value.longValue()); + Instant deviceGoneGracePeriod = Instant.now().minus(config.keepAlive, ChronoUnit.MILLIS) + .minus(DEFAULT_KEEP_ALIVE_CONSIDERED_LOST_IN_MILLISECONDS, ChronoUnit.MILLIS); + if (lastKeepAliveMessageTimestamp.isBefore(deviceGoneGracePeriod)) { + logger.debug( + "Last KeepAlive message received '{}', over grace-period by '{}', consider '{}' gone, setting OFFLINE and disposing", + lastKeepAliveMessageTimestamp, + Duration.between(lastKeepAliveMessageTimestamp, deviceGoneGracePeriod), + thing.getThingTypeUID()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/message.processor.connection.error.keep-alive"); + // Connection lost, avoid sending unsubscription messages + udpSenderActive = false; + disconnect(); + scheduleConnectRetry(retryConnectInMinutes); + } + } + } else if (ThingStatus.OFFLINE.equals(getThing().getStatusInfo().getStatus())) { + logger.debug("Keep alive pool job, '{}' is '{}'", getThing().getThingTypeUID(), + getThing().getStatusInfo().getStatus()); + } + } + + private void handleStatusUpdate(EmotivaUdpResponse emotivaUdpResponse) { + udpSenderActive = true; + logger.debug("Received data from '{}' with length '{}'", emotivaUdpResponse.ipAddress(), + emotivaUdpResponse.answer().length()); + + Object object; + try { + object = xmlUtils.unmarshallToEmotivaDTO(emotivaUdpResponse.answer()); + } catch (JAXBException e) { + logger.debug("Could not unmarshal answer from '{}' with length '{}' and content '{}'", + emotivaUdpResponse.ipAddress(), emotivaUdpResponse.answer().length(), emotivaUdpResponse.answer(), + e); + return; + } + + if (object instanceof EmotivaAckDTO answerDto) { + // Currently not supported to revert a failed command update, just used for logging for now. + logger.trace("Processing received '{}' with '{}'", EmotivaAckDTO.class.getSimpleName(), answerDto); + + } else if (object instanceof EmotivaBarNotifyWrapper answerDto) { + logger.trace("Processing received '{}' with '{}'", EmotivaBarNotifyWrapper.class.getSimpleName(), + emotivaUdpResponse.answer()); + + List emotivaBarNotifies = xmlUtils.unmarshallToBarNotify(answerDto.getTags()); + + if (!emotivaBarNotifies.isEmpty()) { + if (emotivaBarNotifies.get(0).getType() != null) { + findChannelDatatypeAndUpdateChannel(CHANNEL_BAR, emotivaBarNotifies.get(0).formattedMessage(), + STRING); + } + } + } else if (object instanceof EmotivaNotifyWrapper answerDto) { + logger.trace("Processing received '{}' with '{}'", EmotivaNotifyWrapper.class.getSimpleName(), + emotivaUdpResponse.answer()); + handleNotificationUpdate(answerDto); + } else if (object instanceof EmotivaUpdateResponse answerDto) { + logger.trace("Processing received '{}' with '{}'", EmotivaUpdateResponse.class.getSimpleName(), + emotivaUdpResponse.answer()); + handleNotificationUpdate(answerDto); + } else if (object instanceof EmotivaMenuNotifyDTO answerDto) { + logger.trace("Processing received '{}' with '{}'", EmotivaMenuNotifyDTO.class.getSimpleName(), + emotivaUdpResponse.answer()); + + if (answerDto.getRow() != null) { + handleMenuNotify(answerDto); + } else if (answerDto.getProgress() != null && answerDto.getProgress().getTime() != null) { + logger.trace("Processing received '{}' with '{}'", EmotivaMenuNotifyDTO.class.getSimpleName(), + emotivaUdpResponse.answer()); + listeningThreadFactory + .newThread(() -> handleMenuNotifyProgressMessage(answerDto.getProgress().getTime())).start(); + } + } else if (object instanceof EmotivaSubscriptionResponse answerDto) { + logger.trace("Processing received '{}' with '{}'", EmotivaSubscriptionResponse.class.getSimpleName(), + emotivaUdpResponse.answer()); + // Populates static input sources, except input + sourcesMainZone.putAll(EmotivaControlCommands.getCommandsFromType(EmotivaCommandType.SOURCE_MAIN_ZONE)); + sourcesMainZone.remove(EmotivaControlCommands.input); + commandMaps.put(MAP_SOURCES_MAIN_ZONE, sourcesMainZone); + + sourcesZone2.putAll(EmotivaControlCommands.getCommandsFromType(EmotivaCommandType.SOURCE_ZONE2)); + sourcesZone2.remove(EmotivaControlCommands.zone2_input); + commandMaps.put(MAP_SOURCES_ZONE_2, sourcesZone2); + + if (answerDto.getProperties() == null) { + for (EmotivaNotifyDTO dto : xmlUtils.unmarshallToNotification(answerDto.getTags())) { + handleChannelUpdate(dto.getName(), dto.getValue(), dto.getVisible(), dto.getAck()); + } + } else { + for (EmotivaPropertyDTO property : answerDto.getProperties()) { + handleChannelUpdate(property.getName(), property.getValue(), property.getVisible(), + property.getStatus()); + } + } + } + } + + private void handleMenuNotify(EmotivaMenuNotifyDTO answerDto) { + String highlightValue = ""; + + for (var row = 4; row <= 6; row++) { + var emotivaMenuRow = answerDto.getRow().get(row); + logger.debug("Checking row '{}' with '{}' columns", row, emotivaMenuRow.getCol().size()); + for (var column = 0; column <= 2; column++) { + var emotivaMenuCol = emotivaMenuRow.getCol().get(column); + String cellValue = ""; + if (emotivaMenuCol.getValue() != null) { + cellValue = emotivaMenuCol.getValue(); + } + + if (emotivaMenuCol.getCheckbox() != null) { + cellValue = MENU_PANEL_CHECKBOX_ON.equalsIgnoreCase(emotivaMenuCol.getCheckbox().trim()) ? "☑" + : "☐"; + } + + if (emotivaMenuCol.getHighlight() != null + && MENU_PANEL_HIGHLIGHTED.equalsIgnoreCase(emotivaMenuCol.getHighlight().trim())) { + logger.debug("Highlight is at row '{}' column '{}' value '{}'", row, column, cellValue); + highlightValue = cellValue; + } + + var channelName = format("%s-%s-%s", CHANNEL_MENU_DISPLAY_PREFIX, getMenuPanelRowLabel(row), + getMenuPanelColumnLabel(column)); + updateChannelState(channelName, new StringType(cellValue)); + } + } + updateChannelState(CHANNEL_MENU_DISPLAY_HIGHLIGHT, new StringType(highlightValue)); + } + + private void handleMenuNotifyProgressMessage(String progressBarTimeInSeconds) { + try { + var seconds = Integer.parseInt(progressBarTimeInSeconds); + for (var count = 0; seconds >= count; count++) { + updateChannelState(CHANNEL_MENU_DISPLAY_HIGHLIGHT, + new StringType(updateProgress(EmotivaCommandHelper.integerToPercentage(count)))); + } + } catch (NumberFormatException e) { + logger.debug("Menu progress bar time value '{}' is not a valid integer", progressBarTimeInSeconds); + } + } + + private void resetMenuPanelChannels() { + logger.debug("Resetting Menu Panel Display"); + for (var row = 4; row <= 6; row++) { + for (var column = 0; column <= 2; column++) { + var channelName = format("%s-%s-%s", CHANNEL_MENU_DISPLAY_PREFIX, getMenuPanelRowLabel(row), + getMenuPanelColumnLabel(column)); + updateChannelState(channelName, new StringType("")); + } + } + updateChannelState(CHANNEL_MENU_DISPLAY_HIGHLIGHT, new StringType("")); + } + + private void sendEmotivaUpdate(EmotivaControlCommands tags) { + EmotivaUdpSendingService localSendingService = sendingService; + if (localSendingService != null) { + try { + localSendingService.sendUpdate(tags, config); + } catch (InterruptedIOException e) { + logger.debug("Interrupted during sending of EmotivaUpdate message to device '{}'", + this.getThing().getThingTypeUID(), e); + } catch (IOException e) { + logger.error("Failed to send EmotivaUpdate message to device '{}'", this.getThing().getThingTypeUID(), + e); + } + } + } + + private void handleNotificationUpdate(AbstractNotificationDTO answerDto) { + if (answerDto.getProperties() == null) { + for (EmotivaNotifyDTO tag : xmlUtils.unmarshallToNotification(answerDto.getTags())) { + try { + EmotivaSubscriptionTags tagName = EmotivaSubscriptionTags.valueOf(tag.getName()); + if (EmotivaSubscriptionTags.hasChannel(tag.getName())) { + findChannelDatatypeAndUpdateChannel(tagName.getChannel(), tag.getValue(), + tagName.getDataType()); + } + } catch (IllegalArgumentException e) { + logger.debug("Subscription name '{}' could not be mapped to a channel", tag.getName()); + } + } + } else { + for (EmotivaPropertyDTO property : answerDto.getProperties()) { + handleChannelUpdate(property.getName(), property.getValue(), property.getVisible(), + property.getStatus()); + } + } + } + + private void handleChannelUpdate(String emotivaSubscriptionName, String value, String visible, String status) { + logger.debug("Handling channel update for '{}' with value '{}'", emotivaSubscriptionName, value); + + if (status.equals(NOT_VALID.name())) { + logger.debug("Subscription property '{}' not present in device, skipping", emotivaSubscriptionName); + return; + } + + if ("None".equals(value)) { + logger.debug("No value present for channel {}, usually means a speaker is not enabled", + emotivaSubscriptionName); + return; + } + + try { + EmotivaSubscriptionTags.hasChannel(emotivaSubscriptionName); + } catch (IllegalArgumentException e) { + logger.debug("Subscription property '{}' is not know to the binding, might need updating", + emotivaSubscriptionName); + return; + } + + if (noSubscriptionToChannel().contains(EmotivaSubscriptionTags.valueOf(emotivaSubscriptionName))) { + logger.debug("Initial subscription status update for {}, skipping, only want notifications", + emotivaSubscriptionName); + return; + } + + try { + EmotivaSubscriptionTags subscriptionTag; + try { + subscriptionTag = EmotivaSubscriptionTags.valueOf(emotivaSubscriptionName); + } catch (IllegalArgumentException e) { + logger.debug("Property '{}' could not be mapped subscription tag, skipping", emotivaSubscriptionName); + return; + } + + if (subscriptionTag.getChannel().isEmpty()) { + logger.debug("Subscription property '{}' does not have a corresponding channel, skipping", + emotivaSubscriptionName); + return; + } + + String trimmedValue = value.trim(); + + logger.debug("Found subscription '{}' for '{}' and value '{}'", subscriptionTag, emotivaSubscriptionName, + trimmedValue); + + // Add/Update user assigned name for inputs + if (subscriptionTag.getChannel().startsWith(CHANNEL_INPUT1.substring(0, CHANNEL_INPUT1.indexOf("-") + 1)) + && "true".equals(visible)) { + logger.debug("Adding '{}' to dynamic source input list", trimmedValue); + sourcesMainZone.put(EmotivaControlCommands.matchToInput(subscriptionTag.name()), trimmedValue); + commandMaps.put(MAP_SOURCES_MAIN_ZONE, sourcesMainZone); + + logger.debug("sources list is now {}", sourcesMainZone.size()); + } + + // Add/Update audio modes + if (subscriptionTag.getChannel().startsWith(CHANNEL_MODE + "-") && "true".equals(visible)) { + String modeName = i18nProvider.getText("channel-type.emotiva.selected-mode.option." + + subscriptionTag.getChannel().substring(subscriptionTag.getChannel().indexOf("_") + 1)); + logger.debug("Adding '{} ({})' from channel '{}' to dynamic mode list", trimmedValue, modeName, + subscriptionTag.getChannel()); + modes.put(EmotivaSubscriptionTags.fromChannelUID(subscriptionTag.getChannel()), trimmedValue); + } + + findChannelDatatypeAndUpdateChannel(subscriptionTag.getChannel(), trimmedValue, + subscriptionTag.getDataType()); + } catch (IllegalArgumentException e) { + logger.debug("Error updating subscription property '{}'", emotivaSubscriptionName, e); + } + } + + private void findChannelDatatypeAndUpdateChannel(String channelName, String value, EmotivaDataType dataType) { + switch (dataType) { + case DIMENSIONLESS_DECIBEL -> { + var trimmedString = value.replaceAll("[ +]", ""); + logger.debug("Update channel '{}' to '{}:{}'", channelName, QuantityType.class.getSimpleName(), + trimmedString); + if (channelName.equals(CHANNEL_MAIN_VOLUME)) { + updateVolumeChannels(trimmedString, CHANNEL_MUTE, channelName, CHANNEL_MAIN_VOLUME_DB); + } else if (channelName.equals(CHANNEL_ZONE2_VOLUME)) { + updateVolumeChannels(trimmedString, CHANNEL_ZONE2_MUTE, channelName, CHANNEL_ZONE2_VOLUME_DB); + } else { + if (trimmedString.equals("None")) { + updateChannelState(channelName, QuantityType.valueOf(0, Units.DECIBEL)); + } else { + updateChannelState(channelName, + QuantityType.valueOf(Double.parseDouble(trimmedString), Units.DECIBEL)); + } + } + } + case DIMENSIONLESS_PERCENT -> { + var trimmedString = value.replaceAll("[ +]", ""); + logger.debug("Update channel '{}' to '{}:{}'", channelName, PercentType.class.getSimpleName(), value); + updateChannelState(channelName, PercentType.valueOf(trimmedString)); + } + case FREQUENCY_HERTZ -> { + logger.debug("Update channel '{}' to '{}:{}'", channelName, Units.HERTZ.getClass().getSimpleName(), + value); + if (!value.isEmpty()) { + // Getting rid of characters and empty space leaves us with the raw frequency + try { + String frequencyString = value.replaceAll("[a-zA-Z ]", ""); + QuantityType hz = QuantityType.valueOf(0, Units.HERTZ); + if (value.contains("AM")) { + hz = QuantityType.valueOf(Double.parseDouble(frequencyString) * 1000, Units.HERTZ); + } else if (value.contains("FM")) { + hz = QuantityType.valueOf(Double.parseDouble(frequencyString) * 1000000, Units.HERTZ); + } + updateChannelState(CHANNEL_TUNER_CHANNEL, hz); + } catch (NumberFormatException e) { + logger.debug("Could not extract radio tuner frequency from '{}'", value); + } + } + } + case GOODBYE -> { + logger.info( + "Received goodbye notification from '{}'; disconnecting and scheduling av connection retry in '{}' minutes", + getThing().getUID(), DEFAULT_RETRY_INTERVAL_MINUTES); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/message.processor.goodbye"); + + // Device gone, sending unsubscription messages not needed + udpSenderActive = false; + disconnect(); + scheduleConnectRetry(retryConnectInMinutes); + } + case NUMBER_TIME -> { + logger.debug("Update channel '{}' to '{}:{}'", channelName, Number.class.getSimpleName(), value); + long nowEpochSecond = Instant.now().getEpochSecond(); + updateChannelState(channelName, new QuantityType<>(nowEpochSecond, Units.SECOND)); + } + case ON_OFF -> { + logger.debug("Update channel '{}' to '{}:{}'", channelName, OnOffType.class.getSimpleName(), value); + OnOffType switchValue = OnOffType.from(value.trim().toUpperCase()); + updateChannelState(channelName, switchValue); + if (switchValue.equals(OnOffType.OFF) && CHANNEL_MENU.equals(channelName)) { + resetMenuPanelChannels(); + } + } + case STRING -> { + logger.debug("Update channel '{}' to '{}:{}'", channelName, StringType.class.getSimpleName(), value); + updateChannelState(channelName, StringType.valueOf(value)); + } + case UNKNOWN -> // Do nothing, types not connect to channels + logger.debug("Channel '{}' with UNKNOWN type and value '{}' was not updated", channelName, value); + default -> { + // datatypes not connect to a channel, so do nothing + } + } + } + + private void updateChannelState(String channelID, State state) { + stateMap.put(channelID, state); + logger.trace("Updating channel '{}' with '{}'", channelID, state); + updateState(channelID, state); + } + + private void updateVolumeChannels(String value, String muteChannel, String volumeChannel, String volumeDbChannel) { + if ("Mute".equals(value)) { + updateChannelState(muteChannel, OnOffType.ON); + } else { + updateChannelState(volumeChannel, volumeDecibelToPercentage(value)); + updateChannelState(volumeDbChannel, QuantityType.valueOf(Double.parseDouble(value), Units.DECIBEL)); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command ohCommand) { + logger.debug("Handling ohCommand '{}:{}' for '{}'", channelUID.getId(), ohCommand, channelUID.getThingUID()); + EmotivaUdpSendingService localSendingService = sendingService; + + if (localSendingService != null) { + EmotivaControlRequest emotivaRequest = channelToControlRequest(channelUID.getId(), commandMaps, + protocolFromConfig(config.protocolVersion)); + if (ohCommand instanceof RefreshType) { + stateMap.remove(channelUID.getId()); + + if (emotivaRequest.getDefaultCommand().equals(none)) { + logger.debug("Found controlCommand 'none' for request '{}' from channel '{}' with RefreshType", + emotivaRequest.getName(), channelUID); + } else { + logger.debug("Sending EmotivaUpdate for '{}'", emotivaRequest); + sendEmotivaUpdate(emotivaRequest.getDefaultCommand()); + } + } else { + try { + EmotivaControlDTO dto = emotivaRequest.createDTO(ohCommand, stateMap.get(channelUID.getId())); + localSendingService.send(dto); + + if (emotivaRequest.getName().equals(EmotivaControlCommands.volume.name())) { + if (ohCommand instanceof PercentType value) { + updateChannelState(CHANNEL_MAIN_VOLUME_DB, + QuantityType.valueOf(volumePercentageToDecibel(value.intValue()), Units.DECIBEL)); + } else if (ohCommand instanceof QuantityType value) { + updateChannelState(CHANNEL_MAIN_VOLUME, volumeDecibelToPercentage(value.toString())); + } + } else if (emotivaRequest.getName().equals(EmotivaControlCommands.zone2_volume.name())) { + if (ohCommand instanceof PercentType value) { + updateChannelState(CHANNEL_ZONE2_VOLUME_DB, + QuantityType.valueOf(volumePercentageToDecibel(value.intValue()), Units.DECIBEL)); + } else if (ohCommand instanceof QuantityType value) { + updateChannelState(CHANNEL_ZONE2_VOLUME, volumeDecibelToPercentage(value.toString())); + } + } else if (ohCommand instanceof OnOffType value) { + if (value.equals(OnOffType.ON) && emotivaRequest.getOnCommand().equals(power_on)) { + localSendingService.sendUpdate(EmotivaSubscriptionTags.speakerChannels(), config); + } + } + } catch (InterruptedIOException e) { + logger.debug("Interrupted during updating state for channel: '{}:{}:{}'", channelUID.getId(), + emotivaRequest.getName(), emotivaRequest.getDataType(), e); + } catch (IOException e) { + logger.error("Failed updating state for channel '{}:{}:{}'", channelUID.getId(), + emotivaRequest.getName(), emotivaRequest.getDataType(), e); + } + } + } + } + + @Override + public void dispose() { + logger.debug("Disposing '{}'", getThing().getUID()); + + disconnect(); + super.dispose(); + } + + private synchronized void disconnect() { + final EmotivaUdpSendingService localSendingService = sendingService; + if (localSendingService != null) { + logger.debug("Disposing active sender"); + if (udpSenderActive) { + try { + // Unsubscribe before disconnect + localSendingService.sendUnsubscribe(generalSubscription); + localSendingService.sendUnsubscribe(nonGeneralSubscriptions); + } catch (IOException e) { + logger.debug("Failed to unsubscribe for '{}'", config.ipAddress, e); + } + } + + sendingService = null; + try { + localSendingService.disconnect(); + logger.debug("Disconnected udp send connector"); + } catch (Exception e) { + logger.debug("Failed to close socket connection for '{}'", config.ipAddress, e); + } + } + udpSenderActive = false; + + final EmotivaUdpReceivingService notifyConnector = notifyListener; + if (notifyConnector != null) { + notifyListener = null; + try { + notifyConnector.disconnect(); + logger.debug("Disconnected notify connector"); + } catch (Exception e) { + logger.error("Failed to close socket connection for: '{}:{}'", config.ipAddress, config.notifyPort, e); + } + } + + final EmotivaUdpReceivingService menuConnector = menuNotifyListener; + if (menuConnector != null) { + menuNotifyListener = null; + try { + menuConnector.disconnect(); + logger.debug("Disconnected menu notify connector"); + } catch (Exception e) { + logger.error("Failed to close socket connection for: '{}:{}'", config.ipAddress, config.notifyPort, e); + } + } + + ScheduledFuture localConnectRetryJob = this.connectRetryJob; + if (localConnectRetryJob != null) { + localConnectRetryJob.cancel(true); + this.connectRetryJob = null; + } + + ScheduledFuture localPollingJob = this.pollingJob; + if (localPollingJob != null) { + localPollingJob.cancel(true); + this.pollingJob = null; + logger.debug("Polling job canceled"); + } + } + + @Override + public Collection> getServices() { + return Set.of(InputStateOptionProvider.class); + } + + public EnumMap getSourcesMainZone() { + return sourcesMainZone; + } + + public EnumMap getSourcesZone2() { + return sourcesZone2; + } + + public EnumMap getModes() { + return modes; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaTranslationProvider.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaTranslationProvider.java new file mode 100644 index 00000000000..3724d8c42a2 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaTranslationProvider.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * This class provides translated texts. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +@Component(service = EmotivaTranslationProvider.class) +public class EmotivaTranslationProvider { + + private final Bundle bundle; + private final TranslationProvider i18nProvider; + private final LocaleProvider localeProvider; + + @Activate + public EmotivaTranslationProvider(@Reference TranslationProvider i18nProvider, + @Reference LocaleProvider localeProvider) { + this.bundle = FrameworkUtil.getBundle(this.getClass()); + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; + } + + public EmotivaTranslationProvider(final EmotivaTranslationProvider other) { + this.bundle = other.bundle; + this.i18nProvider = other.i18nProvider; + this.localeProvider = other.localeProvider; + } + + public String getText(String key, @Nullable Object... arguments) { + Locale locale = localeProvider.getLocale(); + String message = i18nProvider.getText(bundle, key, this.getDefaultText(key), locale, arguments); + if (message != null) { + return message; + } + return key; + } + + public @Nullable String getDefaultText(String key) { + return i18nProvider.getText(bundle, key, key, Locale.ENGLISH); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpBroadcastService.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpBroadcastService.java new file mode 100644 index 00000000000..c41cfcbec46 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpBroadcastService.java @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V3; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketTimeoutException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.Optional; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.emotiva.internal.dto.EmotivaPingDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaTransponderDTO; +import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils; +import org.openhab.core.common.AbstractUID; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This service is used for discovering Emotiva devices via sending an EmotivaPing UDP message. + * + * @author Hilbrand Bouwkamp - Initial contribution + * @author Espen Fossen - Adapted to Emotiva binding + */ +@NonNullByDefault +public class EmotivaUdpBroadcastService { + + private final Logger logger = LoggerFactory.getLogger(EmotivaUdpBroadcastService.class); + private static final int MAX_PACKET_SIZE = 512; + @Nullable + private DatagramSocket discoverSocket; + private final EmotivaXmlUtils xmlUtils = new EmotivaXmlUtils(); + + /** + * The address to broadcast EmotivaPing message to. + */ + private final String broadcastAddress; + + public EmotivaUdpBroadcastService(String broadcastAddress) throws IllegalArgumentException, JAXBException { + if (broadcastAddress.trim().isEmpty()) { + throw new IllegalArgumentException("Missing broadcast address"); + } + this.broadcastAddress = broadcastAddress; + } + + /** + * Performs the actual discovery of Emotiva devices (things). + */ + public Optional discoverThings() { + try { + final DatagramPacket receivePacket = new DatagramPacket(new byte[MAX_PACKET_SIZE], MAX_PACKET_SIZE); + // No need to call close first, because the caller of this method already has done it. + + discoverSocket = new DatagramSocket( + new InetSocketAddress(EmotivaBindingConstants.DEFAULT_TRANSPONDER_PORT)); + final InetAddress broadcast = InetAddress.getByName(broadcastAddress); + + byte[] emotivaPingDTO = xmlUtils.marshallEmotivaDTO(new EmotivaPingDTO(PROTOCOL_V3.name())) + .getBytes(Charset.defaultCharset()); + final DatagramPacket discoverPacket = new DatagramPacket(emotivaPingDTO, emotivaPingDTO.length, broadcast, + EmotivaBindingConstants.DEFAULT_PING_PORT); + + DatagramSocket localDatagramSocket = discoverSocket; + while (localDatagramSocket != null && discoverSocket != null) { + localDatagramSocket.setBroadcast(true); + localDatagramSocket.setSoTimeout(DEFAULT_UDP_SENDING_TIMEOUT); + localDatagramSocket.send(discoverPacket); + if (logger.isTraceEnabled()) { + logger.trace("Discovery package sent: {}", + new String(discoverPacket.getData(), StandardCharsets.UTF_8)); + } + + // Runs until the socket call gets a timeout and throws an exception. When a timeout is triggered it + // means + // no data was present and nothing new to discover. + while (true) { + // Set packet length in case a previous call reduced the size. + receivePacket.setLength(MAX_PACKET_SIZE); + if (discoverSocket == null) { + break; + } else { + localDatagramSocket.receive(receivePacket); + } + logger.debug("Emotiva device discovery returned package with length '{}'", + receivePacket.getLength()); + if (receivePacket.getLength() > 0) { + return thingDiscovered(receivePacket); + } + } + } + } catch (SocketTimeoutException e) { + logger.debug("Discovering poller timeout..."); + } catch (InterruptedIOException e) { + logger.debug("Interrupted during discovery: {}", e.getMessage()); + } catch (IOException e) { + logger.debug("Error during discovery: {}", e.getMessage()); + } finally { + closeDiscoverSocket(); + } + return Optional.empty(); + } + + /** + * Closes the discovery socket and cleans the value. No need for synchronization as this method is called from a + * synchronized context. + */ + public void closeDiscoverSocket() { + final DatagramSocket localDiscoverSocket = discoverSocket; + if (localDiscoverSocket != null) { + discoverSocket = null; + if (!localDiscoverSocket.isClosed()) { + localDiscoverSocket.close(); // this interrupts and terminates the listening thread + } + } + } + + /** + * Register a device (thing) with the discovered properties. + * + * @param packet containing data of detected device + */ + private Optional thingDiscovered(DatagramPacket packet) { + final String ipAddress = packet.getAddress().getHostAddress(); + String udpResponse = new String(packet.getData(), 0, packet.getLength() - 1, StandardCharsets.UTF_8); + + Object object; + try { + object = xmlUtils.unmarshallToEmotivaDTO(udpResponse); + } catch (JAXBException e) { + logger.debug("Could not unmarshal '{}:{}'", ipAddress, udpResponse.length()); + return Optional.empty(); + } + + if (object instanceof EmotivaTransponderDTO answerDto) { + logger.trace("Processing Received '{}' with '{}' ", EmotivaTransponderDTO.class.getSimpleName(), + udpResponse); + final ThingUID thingUid = new ThingUID( + THING_PROCESSOR + AbstractUID.SEPARATOR + ipAddress.replace(".", "")); + final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUid) + .withThingType(THING_PROCESSOR).withProperty("ipAddress", ipAddress) + .withProperty("controlPort", answerDto.getControl().getControlPort()) + .withProperty("notifyPort", answerDto.getControl().getNotifyPort()) + .withProperty("infoPort", answerDto.getControl().getInfoPort()) + .withProperty("setupPortTCP", answerDto.getControl().getSetupPortTCP()) + .withProperty("menuNotifyPort", answerDto.getControl().getMenuNotifyPort()) + .withProperty("model", answerDto.getModel()) + .withProperty("revision", Objects.requireNonNullElse(answerDto.getRevision(), "")) + .withProperty("dataRevision", Objects.requireNonNullElse(answerDto.getDataRevision(), "")) + .withProperty("protocolVersion", + Objects.requireNonNullElse(answerDto.getControl().getVersion(), + DEFAULT_EMOTIVA_PROTOCOL_VERSION)) + .withProperty("keepAlive", answerDto.getControl().getKeepAlive()) + .withProperty(EmotivaBindingConstants.UNIQUE_PROPERTY_NAME, ipAddress) + .withLabel(answerDto.getName()) + .withRepresentationProperty(EmotivaBindingConstants.UNIQUE_PROPERTY_NAME).build(); + try { + logger.debug("Adding newly discovered thing '{}:{}' with properties '{}'", THING_PROCESSOR, ipAddress, + discoveryResult.getProperties()); + + return Optional.of(discoveryResult); + } catch (Exception e) { + logger.debug("Failed adding discovered thing '{}:{}' with properties '{}'", THING_PROCESSOR, ipAddress, + discoveryResult.getProperties(), e); + } + } else { + logger.debug("Received message of unknown type in message '{}'", udpResponse); + } + return Optional.empty(); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpReceivingService.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpReceivingService.java new file mode 100644 index 00000000000..954af61a2f3 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpReceivingService.java @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.SocketException; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.emotiva.internal.protocol.EmotivaUdpResponse; +import org.openhab.core.common.NamedThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This service is used for receiving UDP message from Emotiva devices. + * + * @author Patrick Koenemann - Initial contribution + * @author Espen Fossen - Adapted to Emotiva binding + */ +@NonNullByDefault +public class EmotivaUdpReceivingService { + + private final Logger logger = LoggerFactory.getLogger(EmotivaUdpReceivingService.class); + + /** + * Buffer for incoming UDP packages. + */ + private static final int MAX_PACKET_SIZE = 10240; + + /** + * The device IP this connector is listening to / sends to. + */ + private final String ipAddress; + + /** + * The port this connector is listening to notify message. + */ + private final int receivingPort; + + /** + * Service to spawn new threads for handling status updates. + */ + private final ExecutorService executorService; + + /** + * Thread factory for UDP listening thread. + */ + private final NamedThreadFactory listeningThreadFactory = new NamedThreadFactory(EmotivaBindingConstants.BINDING_ID, + true); + + /** + * Socket for receiving Notify UDP packages. + */ + private @Nullable DatagramSocket receivingSocket = null; + + /** + * The listener that gets notified upon newly received messages. + */ + private @Nullable Consumer listener; + + private int receiveNotifyFailures = 0; + private boolean listenerNotifyActive = false; + + /** + * Create a listener to an Emotiva device via the given configuration. + * + * @param receivingPort listening port + * @param config Emotiva configuration values + */ + public EmotivaUdpReceivingService(int receivingPort, EmotivaConfiguration config, ExecutorService executorService) { + if (receivingPort <= 0) { + throw new IllegalArgumentException("Invalid receivingPort: " + receivingPort); + } + if (config.ipAddress.trim().isEmpty()) { + throw new IllegalArgumentException("Missing ipAddress"); + } + this.ipAddress = config.ipAddress; + this.receivingPort = receivingPort; + this.executorService = executorService; + } + + /** + * Initialize socket connection to the UDP receive port for the given listener. + * + * @throws SocketException Is only thrown if logNotThrowException = false. + * @throws InterruptedException Typically happens during shutdown. + */ + public void connect(Consumer listener, boolean logNotThrowException) + throws SocketException, InterruptedException { + if (receivingSocket == null) { + try { + receivingSocket = new DatagramSocket(receivingPort); + + this.listener = listener; + + listeningThreadFactory.newThread(this::listen).start(); + + // wait for the listening thread to be active + for (int i = 0; i < 20 && !listenerNotifyActive; i++) { + Thread.sleep(100); // wait at most 20 * 100ms = 2sec for the listener to be active + } + if (!listenerNotifyActive) { + logger.warn( + "Listener thread started but listener is not yet active after 2sec; something seems to be wrong with the JVM thread handling?!"); + } + } catch (SocketException e) { + if (logNotThrowException) { + logger.warn("Failed to open socket connection on port '{}'", receivingPort); + } + + disconnect(); + + if (!logNotThrowException) { + throw e; + } + } + } else if (!Objects.equals(this.listener, listener)) { + throw new IllegalStateException("A listening thread is already running"); + } + } + + private void listen() { + try { + listenUnhandledInterruption(); + } catch (InterruptedException e) { + // OH shutdown - don't log anything, just quit + } + } + + private void listenUnhandledInterruption() throws InterruptedException { + logger.debug("Emotiva listener started for: '{}:{}'", ipAddress, receivingPort); + + final Consumer localListener = listener; + final DatagramSocket localReceivingSocket = receivingSocket; + while (localListener != null && localReceivingSocket != null && receivingSocket != null) { + try { + final DatagramPacket answer = new DatagramPacket(new byte[MAX_PACKET_SIZE], MAX_PACKET_SIZE); + + listenerNotifyActive = true; + localReceivingSocket.receive(answer); // receive packet (blocking call) + listenerNotifyActive = false; + + final byte[] receivedData = Arrays.copyOfRange(answer.getData(), 0, answer.getLength() - 1); + + if (receivedData.length == 0) { + if (isConnected()) { + logger.debug("Nothing received, this may happen during shutdown or some unknown error"); + } + continue; + } + receiveNotifyFailures = 0; // message successfully received, unset failure counter + + handleReceivedData(answer, receivedData, localListener); + } catch (Exception e) { + listenerNotifyActive = false; + + if (receivingSocket == null) { + logger.debug("Socket closed; stopping listener on port '{}'", receivingPort); + } else { + logger.debug("Checkin receiveFailures count {}", receiveNotifyFailures); + // if we get 3 errors in a row, we should better add a delay to stop spamming the log! + if (receiveNotifyFailures++ > EmotivaBindingConstants.DEFAULT_CONNECTION_RETRIES) { + logger.debug( + "Unexpected error while listening on port '{}'; waiting 10sec before the next attempt to listen on that port", + receivingPort, e); + for (int i = 0; i < 50 && receivingSocket != null; i++) { + Thread.sleep(200); // 50 * 200ms = 10sec + } + } else { + logger.debug("Unexpected error while listening on port '{}'", receivingPort, e); + } + } + } + } + } + + private void handleReceivedData(DatagramPacket answer, byte[] receivedData, + Consumer localListener) { + // log & notify listener in new thread (so that listener loop continues immediately) + executorService.execute(() -> { + if (answer.getAddress() != null && answer.getLength() > 0) { + logger.trace("Received data on port '{}': {}", answer.getPort(), receivedData); + EmotivaUdpResponse emotivaUdpResponse = new EmotivaUdpResponse( + new String(answer.getData(), 0, answer.getLength()), answer.getAddress().getHostAddress()); + localListener.accept(emotivaUdpResponse); + } + }); + } + + /** + * Close the socket connection. + */ + public void disconnect() { + logger.debug("Emotiva listener stopped for: '{}:{}'", ipAddress, receivingPort); + listener = null; + final DatagramSocket localReceivingSocket = receivingSocket; + if (localReceivingSocket != null) { + receivingSocket = null; + if (!localReceivingSocket.isClosed()) { + localReceivingSocket.close(); // this interrupts and terminates the listening thread + } + } + } + + public boolean isConnected() { + return receivingSocket != null; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpSendingService.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpSendingService.java new file mode 100644 index 00000000000..5b91a6896f2 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpSendingService.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_UDP_SENDING_TIMEOUT; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionRequest; +import org.openhab.binding.emotiva.internal.dto.EmotivaUnsubscribeDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateRequest; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; +import org.openhab.binding.emotiva.internal.protocol.EmotivaUdpResponse; +import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This service handles sending UDP message to Emotiva devices. + * + * @author Patrick Koenemann - Initial contribution + * @author Espen Fossen - Adapted to Emotiva binding + */ +@NonNullByDefault +public class EmotivaUdpSendingService { + + private final Logger logger = LoggerFactory.getLogger(EmotivaUdpSendingService.class); + + /** + * Buffer for incoming UDP packages. + */ + private static final int MAX_PACKET_SIZE = 10240; + + /** + * The device IP this connector is listening to / sends to. + */ + private final String ipAddress; + + /** + * The port this connector is sending to. + */ + private final int sendingControlPort; + + /** + * Service to spawn new threads for handling status updates. + */ + private final ExecutorService executorService; + + /** + * Socket for sending UDP packages. + */ + private @Nullable DatagramSocket sendingSocket = null; + + /** + * Sending response listener. + */ + private @Nullable Consumer listener; + + private final EmotivaXmlUtils emotivaXmlUtils; + + /** + * Create a socket for sending message to Emotiva device via the given configuration. + * + * @param config Emotiva configuration values + */ + public EmotivaUdpSendingService(EmotivaConfiguration config, ExecutorService executorService) throws JAXBException { + if (config.controlPort <= 0) { + throw new IllegalArgumentException("Invalid udpSendingControlPort: " + config.controlPort); + } + if (config.ipAddress.trim().isEmpty()) { + throw new IllegalArgumentException("Missing ipAddress"); + } + this.ipAddress = config.ipAddress; + this.sendingControlPort = config.controlPort; + this.executorService = executorService; + this.emotivaXmlUtils = new EmotivaXmlUtils(); + } + + /** + * Initialize socket connection to the UDP sending port + * + * @throws SocketException Is only thrown if logNotThrowException = false. + * @throws InterruptedException Typically happens during shutdown. + */ + public void connect(Consumer listener, boolean logNotThrowException) + throws SocketException, InterruptedException { + try { + sendingSocket = new DatagramSocket(sendingControlPort); + + this.listener = listener; + } catch (SocketException e) { + disconnect(); + + if (!logNotThrowException) { + throw e; + } + } + } + + private void handleReceivedData(DatagramPacket answer, byte[] receivedData, + Consumer localListener) { + // log & notify listener in new thread (so that listener loop continues immediately) + executorService.execute(() -> { + if (answer.getAddress() != null && answer.getLength() > 0) { + logger.trace("Received data on port '{}': {}", answer.getPort(), receivedData); + EmotivaUdpResponse emotivaUdpResponse = new EmotivaUdpResponse( + new String(answer.getData(), 0, answer.getLength()), answer.getAddress().getHostAddress()); + localListener.accept(emotivaUdpResponse); + } + }); + } + + /** + * Close the socket connection. + */ + public void disconnect() { + logger.debug("Emotiva sender stopped for '{}'", ipAddress); + listener = null; + final DatagramSocket localSendingSocket = sendingSocket; + if (localSendingSocket != null) { + synchronized (this) { + if (Objects.equals(sendingSocket, localSendingSocket)) { + sendingSocket = null; + if (!localSendingSocket.isClosed()) { + localSendingSocket.close(); + } + } + } + } + } + + public void send(EmotivaControlDTO dto) throws IOException { + send(emotivaXmlUtils.marshallJAXBElementObjects(dto)); + } + + public void sendSubscription(EmotivaSubscriptionTags[] tags, EmotivaConfiguration config) throws IOException { + send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaSubscriptionRequest(tags, config.protocolVersion))); + } + + public void sendUpdate(EmotivaControlCommands defaultCommand, EmotivaConfiguration config) throws IOException { + send(emotivaXmlUtils + .marshallJAXBElementObjects(new EmotivaUpdateRequest(defaultCommand, config.protocolVersion))); + } + + public void sendUpdate(EmotivaSubscriptionTags[] tags, EmotivaConfiguration config) throws IOException { + send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaUpdateRequest(tags, config.protocolVersion))); + } + + public void sendUnsubscribe(EmotivaSubscriptionTags[] defaultCommand) throws IOException { + send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaUnsubscribeDTO(defaultCommand))); + } + + public void send(String msg) throws IOException { + logger.trace("Sending message '{}' to {}:{}", msg, ipAddress, sendingControlPort); + if (msg.isEmpty()) { + throw new IllegalArgumentException("Message must not be empty"); + } + + final InetAddress ipAddress = InetAddress.getByName(this.ipAddress); + byte[] buf = msg.getBytes(Charset.defaultCharset()); + DatagramPacket packet = new DatagramPacket(buf, buf.length, ipAddress, sendingControlPort); + + // make sure we are not interrupted by a disconnect while sending this message + synchronized (this) { + DatagramSocket localDatagramSocket = this.sendingSocket; + final DatagramPacket answer = new DatagramPacket(new byte[MAX_PACKET_SIZE], MAX_PACKET_SIZE); + final Consumer localListener = listener; + if (localDatagramSocket != null && !localDatagramSocket.isClosed()) { + localDatagramSocket.setSoTimeout(DEFAULT_UDP_SENDING_TIMEOUT); + localDatagramSocket.send(packet); + logger.debug("Sending successful"); + + localDatagramSocket.receive(answer); + final byte[] receivedData = Arrays.copyOfRange(answer.getData(), 0, answer.getLength() - 1); + + if (receivedData.length == 0) { + logger.debug("Nothing received, this may happen during shutdown or some unknown error"); + } + + if (localListener != null) { + handleReceivedData(answer, receivedData, localListener); + } + } else { + throw new SocketException("Datagram Socket closed or not initialized"); + } + } + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/InputStateOptionProvider.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/InputStateOptionProvider.java new file mode 100644 index 00000000000..c20b2c6f581 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/InputStateOptionProvider.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.BINDING_ID; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SOURCE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_SOURCE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.EMOTIVA_SOURCE_COMMAND_PREFIX; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class provides the list of valid inputs for a source or audio mode. + * + * @author Kai Kreuzer - Initial contribution + * @author Espen Fossen - Adapted to Emotiva binding + */ +@NonNullByDefault +public class InputStateOptionProvider extends BaseDynamicStateDescriptionProvider implements ThingHandlerService { + + private final Logger logger = LoggerFactory.getLogger(InputStateOptionProvider.class); + + private @Nullable EmotivaProcessorHandler handler; + + @Override + public void setThingHandler(ThingHandler handler) { + this.handler = (EmotivaProcessorHandler) handler; + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } + + @Override + public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original, + @Nullable Locale locale) { + ChannelTypeUID typeUID = channel.getChannelTypeUID(); + if (typeUID == null || !BINDING_ID.equals(typeUID.getBindingId()) || original == null) { + return null; + } + + List options = new ArrayList<>(); + EmotivaProcessorHandler localHandler = handler; + if (localHandler != null) { + if (channel.getUID().getId().equals(CHANNEL_SOURCE)) { + setStateOptionsForSource(channel, options, localHandler.getSourcesMainZone()); + } else if (channel.getUID().getId().equals(CHANNEL_ZONE2_SOURCE)) { + setStateOptionsForSource(channel, options, localHandler.getSourcesZone2()); + } else if (channel.getUID().getId().equals(CHANNEL_MODE)) { + EnumMap modes = localHandler.getModes(); + Collection modeKeys = modes.keySet(); + for (EmotivaSubscriptionTags modeKey : modeKeys) { + options.add(new StateOption(modeKey.name(), modes.get(modeKey))); + } + logger.debug("Updated '{}' with '{}'", CHANNEL_MODE, options); + setStateOptions(channel.getUID(), options); + } + } + + return super.getStateDescription(channel, original, locale); + } + + private void setStateOptionsForSource(Channel channel, List options, + EnumMap sources) { + Collection sourceKeys = sources.keySet(); + for (EmotivaControlCommands sourceKey : sourceKeys) { + if (sourceKey.name().startsWith(EMOTIVA_SOURCE_COMMAND_PREFIX)) { + options.add(new StateOption(sourceKey.name(), sources.get(sourceKey))); + } else { + options.add(new StateOption(sourceKey.name(), sourceKey.getLabel())); + } + } + logger.debug("Updated '{}' with '{}'", channel.getUID().getId(), options); + setStateOptions(channel.getUID(), options); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/discovery/EmotivaDiscoveryService.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/discovery/EmotivaDiscoveryService.java new file mode 100644 index 00000000000..3323e8645a6 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/discovery/EmotivaDiscoveryService.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.discovery; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*; + +import java.util.Objects; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.emotiva.internal.EmotivaUdpBroadcastService; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryService; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Discovery service for Emotiva devices. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, configurationPid = "discovery.emotiva") +public class EmotivaDiscoveryService extends AbstractDiscoveryService { + + private final Logger logger = LoggerFactory.getLogger(EmotivaDiscoveryService.class); + + @Nullable + private final EmotivaUdpBroadcastService broadcastService = new EmotivaUdpBroadcastService( + DISCOVERY_BROADCAST_ADDRESS); + + public EmotivaDiscoveryService() throws IllegalArgumentException, JAXBException { + super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS, false); + } + + @Override + protected void startScan() { + logger.debug("Start scan for Emotiva devices"); + EmotivaUdpBroadcastService localBroadcastService = broadcastService; + if (localBroadcastService != null) { + try { + localBroadcastService.discoverThings().ifPresent(this::thingDiscovered); + } finally { + removeOlderResults(getTimestampOfLastScan()); + } + } + } + + @Override + protected void stopScan() { + logger.debug("Stop scan for Emotiva devices"); + Objects.requireNonNull(broadcastService).closeDiscoverSocket(); + super.stopScan(); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractJAXBElementDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractJAXBElementDTO.java new file mode 100644 index 00000000000..cb5e33e2cb3 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractJAXBElementDTO.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.namespace.QName; + +/** + * Defines elements used by common request DTO classes. + * + * @author Espen Fossen - Initial contribution + */ +public class AbstractJAXBElementDTO { + + @XmlTransient + protected List commands; + + @XmlAnyElement + protected List> jaxbElements; + + public List getCommands() { + return commands; + } + + public void setCommands(List commands) { + this.commands = commands; + } + + public void setJaxbElements(List> jaxbElements) { + this.jaxbElements = jaxbElements; + } + + public JAXBElement createJAXBElement(QName name) { + return new JAXBElement(name, String.class, null); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractNotificationDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractNotificationDTO.java new file mode 100644 index 00000000000..afe72d6ca79 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractNotificationDTO.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlElement; + +/** + * Defines elements used by common notification DTO classes. + * + * @author Espen Fossen - Initial contribution + */ +public class AbstractNotificationDTO { + + // Only present with PROTOCOL_V2 or older + @XmlAnyElement(lax = true) + List tags; + + // Only present with PROTOCOL_V3 or newer + @XmlElement(name = "property") + List properties; + + public List getProperties() { + return properties; + } + + public List getTags() { + return tags; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/ControlDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/ControlDTO.java new file mode 100644 index 00000000000..ed6de5ae444 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/ControlDTO.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Emotiva Control XML object, which is part of the {@link EmotivaTransponderDTO} response. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "control") +@XmlAccessorType(XmlAccessType.FIELD) +public class ControlDTO { + + @XmlElement(name = "version") + String version; + @XmlElement(name = "controlPort") + int controlPort; + @XmlElement(name = "notifyPort") + int notifyPort; + @XmlElement(name = "infoPort") + int infoPort; + @XmlElement(name = "menuNotifyPort") + int menuNotifyPort; + @XmlElement(name = "setupPortTCP") + int setupPortTCP; + @XmlElement(name = "setupXMLVersion") + int setupXMLVersion; + @XmlElement(name = "keepAlive") + int keepAlive; + + public ControlDTO() { + } + + public String getVersion() { + return version; + } + + public int getControlPort() { + return controlPort; + } + + public int getNotifyPort() { + return notifyPort; + } + + public int getInfoPort() { + return infoPort; + } + + public int getMenuNotifyPort() { + return menuNotifyPort; + } + + public int getSetupPortTCP() { + return setupPortTCP; + } + + public int getSetupXMLVersion() { + return setupXMLVersion; + } + + public int getKeepAlive() { + return keepAlive; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTO.java new file mode 100644 index 00000000000..92d76d53d06 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTO.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * The EmotivaAck message type. Received from Emotiva device whenever a {@link EmotivaControlDTO} with + * {@link EmotivaCommandDTO} is sent. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaAck") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaAckDTO { + + @XmlAnyElement(lax = true) + private List commands; + + @SuppressWarnings("unused") + public EmotivaAckDTO() { + } + + public List getCommands() { + return commands; + } + + public void setCommands(List commands) { + this.commands = commands; + } + + @Override + public String toString() { + return "EmotivaAckDTO{" + "commands=" + commands + '}'; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTO.java new file mode 100644 index 00000000000..e94250b7153 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTO.java @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlValue; + +/** + * The EmotivaBarNotify message type. Received from a device if subscribed to the + * {@link org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags#bar_update} type. Uses the + * {@link EmotivaBarNotifyWrapper} to handle unmarshalling. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "property") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaBarNotifyDTO { + + @XmlValue + private String name = "bar"; + + // Possible values “bar”, “centerBar”, “bigText’, “off” + @XmlAttribute + private String type; + @XmlAttribute + private String text; + @XmlAttribute + private String units; + @XmlAttribute + private String value; + @XmlAttribute + private String min; + @XmlAttribute + private String max; + + @SuppressWarnings("unused") + public EmotivaBarNotifyDTO() { + } + + public EmotivaBarNotifyDTO(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getUnits() { + return units; + } + + public void setUnits(String units) { + this.units = units; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getMin() { + return min; + } + + public void setMin(String min) { + this.min = min; + } + + public String getMax() { + return max; + } + + public void setMax(String max) { + this.max = max; + } + + public String formattedMessage() { + StringBuilder sb = new StringBuilder(); + + if (type != null) { + if (!"off".equals(type)) { + if (text != null) { + sb.append(text); + } + if (value != null) { + sb.append(" "); + try { + Double doubleValue = Double.valueOf(value); + sb.append(String.format("%.1f", doubleValue)); + } catch (NumberFormatException e) { + sb.append(value); + } + } + if (units != null) { + sb.append(" ").append(units); + } + } + } + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyWrapper.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyWrapper.java new file mode 100644 index 00000000000..eb35615abc9 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyWrapper.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * A helper class for receiving {@link EmotivaBarNotifyDTO} messages. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaBarNotify") +public class EmotivaBarNotifyWrapper { + + @XmlAttribute + private String sequence; + + @XmlAnyElement(lax = true) + List tags; + + @SuppressWarnings("unused") + public EmotivaBarNotifyWrapper() { + } + + public String getSequence() { + return sequence; + } + + public List getTags() { + return tags; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTO.java new file mode 100644 index 00000000000..9d65b65c788 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTO.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_CONTROL_ACK_VALUE; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlValue; + +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * The EmotivaCommand DTO. Use by multiple message types to control commands in Emotiva devices. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "property") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaCommandDTO { + + @XmlValue + private String commandName; + @XmlAttribute + private String value; + @XmlAttribute + private String visible; + @XmlAttribute + private String status; + @XmlAttribute + private String ack; + + @SuppressWarnings("unused") + public EmotivaCommandDTO() { + } + + public EmotivaCommandDTO(EmotivaControlCommands command) { + this.commandName = command.name(); + } + + public EmotivaCommandDTO(EmotivaSubscriptionTags tag) { + this.commandName = tag.name(); + } + + public EmotivaCommandDTO(EmotivaControlCommands command, String value) { + this.commandName = command.name(); + this.value = value; + } + + public EmotivaCommandDTO(EmotivaControlCommands commandName, String value, String ack) { + this(commandName, value); + this.ack = ack; + } + + /** + * Creates a new instance based on command. Primarily used by EmotivaControl messages. + * + * @return EmotivaCommandDTO with ack=yes always added + */ + public static EmotivaCommandDTO fromTypeWithAck(EmotivaControlCommands command) { + EmotivaCommandDTO emotivaCommandDTO = new EmotivaCommandDTO(command); + emotivaCommandDTO.setAck(DEFAULT_CONTROL_ACK_VALUE); + return emotivaCommandDTO; + } + + /** + * Creates a new instance based on command and value. Primarily used by EmotivaControl messages. + * + * @return EmotivaCommandDTO with ack=yes always added + */ + public static EmotivaCommandDTO fromTypeWithAck(EmotivaControlCommands command, String value) { + EmotivaCommandDTO emotivaCommandDTO = new EmotivaCommandDTO(command); + if (value != null) { + emotivaCommandDTO.setValue(value); + } + emotivaCommandDTO.setAck(DEFAULT_CONTROL_ACK_VALUE); + return emotivaCommandDTO; + } + + public static EmotivaCommandDTO fromType(EmotivaControlCommands command) { + return new EmotivaCommandDTO(command); + } + + public static EmotivaCommandDTO fromType(EmotivaSubscriptionTags command) { + return new EmotivaCommandDTO(command); + } + + public static EmotivaCommandDTO fromTypeWithAck(EmotivaSubscriptionTags command) { + EmotivaCommandDTO emotivaCommandDTO = new EmotivaCommandDTO(command); + emotivaCommandDTO.setAck(DEFAULT_CONTROL_ACK_VALUE); + return emotivaCommandDTO; + } + + public String getName() { + return commandName; + } + + public String getValue() { + return value; + } + + public String getVisible() { + return visible; + } + + public String getStatus() { + return status; + } + + public String getAck() { + return ack; + } + + public void setName(String name) { + this.commandName = name; + } + + public void setValue(String value) { + this.value = value; + } + + public void setVisible(String visible) { + this.visible = visible; + } + + public void setStatus(String status) { + this.status = status; + } + + public void setAck(String ack) { + this.ack = ack; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTO.java new file mode 100644 index 00000000000..3c4e7e1cb4e --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTO.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_CONTROL_MESSAGE_SET_DEFAULT_VALUE; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; + +/** + * The EmotivaControl message type. Use to send commands via {@link EmotivaCommandDTO} to Emotiva devices. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaControl") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaControlDTO extends AbstractJAXBElementDTO { + + @SuppressWarnings("unused") + public EmotivaControlDTO() { + } + + public EmotivaControlDTO(List commands) { + this.commands = commands; + } + + public static EmotivaControlDTO create(EmotivaControlCommands command) { + return new EmotivaControlDTO( + List.of(EmotivaCommandDTO.fromTypeWithAck(command, DEFAULT_CONTROL_MESSAGE_SET_DEFAULT_VALUE))); + } + + public static EmotivaControlDTO create(EmotivaControlCommands command, int value) { + return new EmotivaControlDTO(List.of(EmotivaCommandDTO.fromTypeWithAck(command, String.valueOf(value)))); + } + + public static EmotivaControlDTO create(EmotivaControlCommands command, double value) { + return new EmotivaControlDTO( + List.of(EmotivaCommandDTO.fromTypeWithAck(command, String.valueOf(Math.round(value * 2) / 2.0)))); + } + + public static EmotivaControlDTO create(EmotivaControlCommands command, String value) { + return new EmotivaControlDTO(List.of(EmotivaCommandDTO.fromTypeWithAck(command, value))); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuCol.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuCol.java new file mode 100644 index 00000000000..1061680e278 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuCol.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Data field use by {@link EmotivaMenuNotifyDTO}. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "col") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaMenuCol { + + @XmlAttribute + private String arrow; + @XmlAttribute + private String checkbox; + @XmlAttribute + private String fixed; + @XmlAttribute + private String fixedWidth; + @XmlAttribute + private String highlight; + @XmlAttribute + private String offset; + @XmlAttribute + private String number; + @XmlAttribute + private String value; + + public EmotivaMenuCol() { + } + + public String getArrow() { + return arrow; + } + + public void setArrow(String arrow) { + this.arrow = arrow; + } + + public String getCheckbox() { + return checkbox; + } + + public void setCheckbox(String checkbox) { + this.checkbox = checkbox; + } + + public String getFixed() { + return fixed; + } + + public void setFixed(String fixed) { + this.fixed = fixed; + } + + public String getFixedWidth() { + return fixedWidth; + } + + public void setFixedWidth(String fixedWidth) { + this.fixedWidth = fixedWidth; + } + + public String getHighlight() { + return highlight; + } + + public void setHighlight(String highlight) { + this.highlight = highlight; + } + + public String getOffset() { + return offset; + } + + public void setOffset(String offset) { + this.offset = offset; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTO.java new file mode 100644 index 00000000000..24d0cad750f --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTO.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * The EmotivaMenuNotify message type. Received from a device if subscribed to the + * + * @link EmotivaSubscriptionTags#menu_update} type. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaMenuNotify") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaMenuNotifyDTO { + + @XmlAttribute + private String sequence; + + @XmlElement + private List row; + @XmlElement + private EmotivaMenuProgress progress; + + public EmotivaMenuNotifyDTO() { + } + + public String getSequence() { + return sequence; + } + + public void setSequence(String sequence) { + this.sequence = sequence; + } + + public List getRow() { + return row; + } + + public void setRow(List row) { + this.row = row; + } + + public EmotivaMenuProgress getProgress() { + return progress; + } + + public void setProgress(EmotivaMenuProgress progress) { + this.progress = progress; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuProgress.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuProgress.java new file mode 100644 index 00000000000..f068ffb6dfb --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuProgress.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Data field use by {@link EmotivaMenuNotifyDTO}. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "progress") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaMenuProgress { + + @XmlAttribute + private String time; + + public EmotivaMenuProgress() { + } + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuRow.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuRow.java new file mode 100644 index 00000000000..8683c91fde9 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuRow.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Data field use by {@link EmotivaMenuNotifyDTO}. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "row") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaMenuRow { + + @XmlAttribute + private String number; + + @XmlElement + private List col; + + public EmotivaMenuRow() { + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + public List getCol() { + return col; + } + + public void setCol(List col) { + this.col = col; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyDTO.java new file mode 100644 index 00000000000..af9bb0b2dd4 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyDTO.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlValue; + +/** + * The EmotivaNotify message type. Received from a device if subscribed to + * {@link org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags} values. Uses + * the {@link EmotivaNotifyWrapper} to handle unmarshalling. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "property") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaNotifyDTO { + + @XmlValue + private String tagName; + @XmlAttribute + private String value; + @XmlAttribute + private String visible; + @XmlAttribute + private String status; + @XmlAttribute + private String ack; + + @SuppressWarnings("unused") + public EmotivaNotifyDTO() { + } + + public EmotivaNotifyDTO(String tag) { + this.tagName = tag; + } + + public String getName() { + return tagName; + } + + public String getValue() { + return value; + } + + public String getVisible() { + return visible; + } + + public String getStatus() { + return status; + } + + public void setName(String name) { + this.tagName = name; + } + + public void setValue(String value) { + this.value = value; + } + + public void setVisible(String visible) { + this.visible = visible; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getAck() { + return ack; + } + + public void setAck(String ack) { + this.ack = ack; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapper.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapper.java new file mode 100644 index 00000000000..2d08fc49b7a --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapper.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Emotiva Notify message type. 2.x version of protocol uses command type as prefix in each line in the body, while 3.x + * users property as prefix with name="commandType". 2.x is handled as a element with a special handler unmarshall + * handler in {@link org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils}, while 3.x qualifies as a proper xml + * element and can be properly unmarshalled by + * JAXB. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaNotify") +public class EmotivaNotifyWrapper extends AbstractNotificationDTO { + + @XmlAttribute + private String sequence; + + @SuppressWarnings("unused") + public EmotivaNotifyWrapper() { + } + + public EmotivaNotifyWrapper(String sequence, List properties) { + this.sequence = sequence; + this.properties = properties; + } + + public String getSequence() { + return sequence; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTO.java new file mode 100644 index 00000000000..e1469ad8eda --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTO.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * The EmotivaPing message type. Use to discover Emotiva devices. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaPing") +public class EmotivaPingDTO { + + @XmlAttribute + private String protocol; + + public EmotivaPingDTO() { + } + + public EmotivaPingDTO(String protocol) { + this.protocol = protocol; + } + + public String getProtocol() { + return protocol; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTO.java new file mode 100644 index 00000000000..688d4112350 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTO.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.Objects; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * The EmotivaProperty DTO. Use by multiple message types to get updates from Emotiva devices. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "property") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaPropertyDTO { + + @XmlAttribute + private String name; + @XmlAttribute + private String value; + @XmlAttribute + private String visible; + @XmlAttribute + private String status; + + @SuppressWarnings("unused") + public EmotivaPropertyDTO() { + } + + public EmotivaPropertyDTO(String name, String value, String visible) { + this.name = name; + this.value = value; + this.visible = visible; + } + + public EmotivaPropertyDTO(String name, String value, String visible, String status) { + this.name = name; + this.value = value; + this.visible = visible; + this.status = status; + } + + public String getName() { + return name; + } + + public String getValue() { + return Objects.requireNonNullElse(value, ""); + } + + public String getVisible() { + return Objects.requireNonNullElse(visible, "false"); + } + + public String getStatus() { + return Objects.requireNonNullElse(status, ""); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionDTO.java new file mode 100644 index 00000000000..6bc153646c6 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionDTO.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlValue; + +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * The EmotivaSubscriptionDTO message type. Used to send commands via {@link EmotivaSubscriptionRequest} to Emotiva + * devices. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "property") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaSubscriptionDTO { + + @XmlValue + private String propertyName; + + @SuppressWarnings("unused") + public EmotivaSubscriptionDTO() { + } + + public EmotivaSubscriptionDTO(EmotivaSubscriptionTags property) { + this.propertyName = property.name(); + } + + public static EmotivaSubscriptionDTO fromType(EmotivaSubscriptionTags tag) { + return new EmotivaSubscriptionDTO(tag); + } + + public String getName() { + return propertyName; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequest.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequest.java new file mode 100644 index 00000000000..faff3133202 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequest.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V2; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * A helper class for sending {@link EmotivaSubscriptionDTO} messages. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaSubscription") +public class EmotivaSubscriptionRequest extends AbstractJAXBElementDTO { + + @XmlAttribute + private String protocol = PROTOCOL_V2.value(); + + @SuppressWarnings("unused") + public EmotivaSubscriptionRequest() { + } + + public EmotivaSubscriptionRequest(List commands, String protocol) { + this.protocol = protocol; + this.commands = commands; + } + + public EmotivaSubscriptionRequest(EmotivaSubscriptionTags[] emotivaCommandTypes, String protocol) { + this.protocol = protocol; + List list = new ArrayList<>(); + for (EmotivaSubscriptionTags commandType : emotivaCommandTypes) { + list.add(EmotivaCommandDTO.fromTypeWithAck(commandType)); + } + this.commands = list; + } + + public EmotivaSubscriptionRequest(EmotivaSubscriptionTags tag) { + this.commands = List.of(EmotivaCommandDTO.fromTypeWithAck(tag)); + } + + public EmotivaSubscriptionRequest(EmotivaControlCommands commandType, String protocol) { + this.protocol = protocol; + this.commands = List.of(EmotivaCommandDTO.fromTypeWithAck(commandType)); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponse.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponse.java new file mode 100644 index 00000000000..e2b87f6bec1 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponse.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * A helper class for receiving {@link EmotivaSubscriptionDTO} messages. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaSubscription") +public class EmotivaSubscriptionResponse { + + // Only present with PROTOCOL_V2 or older + @XmlAnyElement(lax = true) + List tags; + + // Only present with PROTOCOL_V3 or newer + @XmlElement(name = "property") + List properties; + + @SuppressWarnings("unused") + public EmotivaSubscriptionResponse() { + } + + public EmotivaSubscriptionResponse(List properties) { + this.properties = properties; + } + + public List getProperties() { + return properties; + } + + public List getTags() { + return tags; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTO.java new file mode 100644 index 00000000000..d31e65dd9c7 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTO.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * The EmotivaTransponder message type. Received from a device if after a successful device discovery via the + * {@link EmotivaPingDTO} message type. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaTransponder") +public class EmotivaTransponderDTO { + + @XmlElement(name = "model") + private String model; + @XmlElement(name = "revision") + private String revision; + @XmlElement(name = "dataRevision") + private String dataRevision; + @XmlElement(name = "name") + private String name; + @XmlElement(name = "control") + private ControlDTO control; + + public java.lang.String getModel() { + return model; + } + + public java.lang.String getRevision() { + return revision; + } + + public String getDataRevision() { + return dataRevision; + } + + public String getName() { + return name; + } + + public ControlDTO getControl() { + return control; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscribeDTO.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscribeDTO.java new file mode 100644 index 00000000000..6a97e53a364 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscribeDTO.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlRootElement; + +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * The EmotivaUnsubscriptionDTO message type. Use to remove subscription after registration via { + * + * @link EmotivaSubscriptionRequest}. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaUnsubscribe") +public class EmotivaUnsubscribeDTO extends AbstractJAXBElementDTO { + + @SuppressWarnings("unused") + public EmotivaUnsubscribeDTO() { + } + + public EmotivaUnsubscribeDTO(List commands) { + this.commands = commands; + } + + public EmotivaUnsubscribeDTO(EmotivaSubscriptionTags[] emotivaCommandTypes) { + List list = new ArrayList<>(); + for (EmotivaSubscriptionTags commandType : emotivaCommandTypes) { + list.add(EmotivaCommandDTO.fromType(commandType)); + } + this.commands = list; + } + + public EmotivaUnsubscribeDTO(EmotivaSubscriptionTags tag) { + this.commands = List.of(EmotivaCommandDTO.fromType(tag)); + } + + public EmotivaUnsubscribeDTO(EmotivaControlCommands commandType) { + this.commands = List.of(EmotivaCommandDTO.fromType(commandType)); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequest.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequest.java new file mode 100644 index 00000000000..0fb3c9fc156 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequest.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * A helper class for sending EmotivaUpdate messages with {@link EmotivaCommandDTO} commands. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaUpdate") +public class EmotivaUpdateRequest extends AbstractJAXBElementDTO { + + @XmlAttribute + private String protocol; + + @SuppressWarnings("unused") + public EmotivaUpdateRequest() { + } + + public EmotivaUpdateRequest(List commands) { + this.commands = commands; + } + + public EmotivaUpdateRequest(EmotivaControlCommands command, String protocol) { + this.protocol = protocol; + List list = new ArrayList<>(); + list.add(EmotivaCommandDTO.fromType(command)); + this.commands = list; + } + + public EmotivaUpdateRequest(EmotivaSubscriptionTags tag) { + this.commands = List.of(EmotivaCommandDTO.fromType(tag)); + } + + public EmotivaUpdateRequest(EmotivaSubscriptionTags[] tags, String protocol) { + this.protocol = protocol; + List list = new ArrayList<>(); + for (EmotivaSubscriptionTags tag : tags) { + list.add(EmotivaCommandDTO.fromType(tag)); + } + this.commands = list; + } + + public EmotivaUpdateRequest(EmotivaControlCommands commandType) { + this.commands = List.of(EmotivaCommandDTO.fromType(commandType)); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponse.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponse.java new file mode 100644 index 00000000000..5b7b5a71fa5 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponse.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * The EmotivaUpdate message type. Received if an {@link EmotivaUpdateRequest} sent to a device. + * + * @author Espen Fossen - Initial contribution + */ +@XmlRootElement(name = "emotivaUpdate") +@XmlAccessorType(XmlAccessType.FIELD) +public class EmotivaUpdateResponse extends AbstractNotificationDTO { + + @SuppressWarnings("unused") + public EmotivaUpdateResponse() { + } + + public EmotivaUpdateResponse(List properties) { + this.properties = properties; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaCommandType.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaCommandType.java new file mode 100644 index 00000000000..2ffda5d9616 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaCommandType.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Enum types for commands to send to Emotiva devices. Used by {@link EmotivaControlRequest} to create correct + * {@link org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO} command message. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum EmotivaCommandType { + + CYCLE, // Cycles to multiple states + NONE, // Unknown or not in use commands + NUMBER, + MENU_CONTROL, + MODE, // Audio mode + SET, // Sets a specific number or string value + SOURCE_MAIN_ZONE, // Main Zone sources + SOURCE_USER, // Source with possible user assigned name + SOURCE_ZONE2, // Zone 2 sources + SPEAKER_PRESET, // Speaker preset + TOGGLE, // Two state toggle + UP_DOWN_SINGLE, // +1/-1 + UP_DOWN_HALF // +0.5/-0.5 + +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlCommands.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlCommands.java new file mode 100644 index 00000000000..ef78e54252a --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlCommands.java @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.CYCLE; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.MENU_CONTROL; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.MODE; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.NONE; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.NUMBER; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SET; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SOURCE_MAIN_ZONE; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SOURCE_USER; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SOURCE_ZONE2; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SPEAKER_PRESET; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.TOGGLE; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.UP_DOWN_HALF; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.UP_DOWN_SINGLE; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.DIMENSIONLESS_DECIBEL; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.DIMENSIONLESS_PERCENT; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.NOT_IMPLEMENTED; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.ON_OFF; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.STRING; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.UNKNOWN; + +import java.util.EnumMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Emotiva command name with corresponding command type and UoM data type. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum EmotivaControlCommands { + none("", NONE, UNKNOWN), + standby("", TOGGLE, ON_OFF), + source_tuner("Tuner", SOURCE_USER, STRING), + source_1("Input 1", SOURCE_USER, STRING), + source_2("Input 2", SOURCE_USER, STRING), + source_3("Input 3", SOURCE_USER, STRING), + source_4("Input 4", SOURCE_USER, STRING), + source_5("Input 5", SOURCE_USER, STRING), + source_6("Input 6", SOURCE_USER, STRING), + source_7("Input 7", SOURCE_USER, STRING), + source_8("Input 8", SOURCE_USER, STRING), + menu("", SET, STRING), + menu_control("", MENU_CONTROL, STRING), // Not an Emotiva command, just a placeholder + up("", SET, STRING), + down("", SET, STRING), + left("", SET, STRING), + right("", SET, STRING), + enter("", SET, STRING), + dim("", CYCLE, DIMENSIONLESS_PERCENT), + mode("", MODE, STRING), + info("", SET, UNKNOWN), + mute("", SET, ON_OFF), + mute_off("", SET, ON_OFF), + mute_on("", SET, ON_OFF), + music("", SET, STRING), + movie("", SET, STRING), + center("", SET, DIMENSIONLESS_DECIBEL), + subwoofer("", SET, DIMENSIONLESS_DECIBEL), + surround("", SET, DIMENSIONLESS_DECIBEL), + back("", SET, DIMENSIONLESS_DECIBEL), + input("", NONE, STRING), + input_up("", SET, STRING), + input_down("", SET, STRING), + power("", TOGGLE, ON_OFF), // Not an Emotiva command, just a placeholder + power_on("", SET, ON_OFF), + power_off("", SET, ON_OFF), + volume("", SET, DIMENSIONLESS_DECIBEL), + set_volume("", NUMBER, DIMENSIONLESS_DECIBEL), + loudness_on("", SET, ON_OFF), + loudness_off("", SET, ON_OFF), + loudness("", TOGGLE, ON_OFF), + speaker_preset("", SPEAKER_PRESET, STRING), + mode_up("", SET, STRING), + mode_down("", SET, STRING), + bass("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL), // Not an Emotiva command, just a placeholder + bass_up("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL), + bass_down("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL), + treble("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL), // Not an Emotiva command, just a placeholder + treble_up("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL), + treble_down("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL), + zone2_power("", TOGGLE, ON_OFF), + zone2_power_off("", SET, ON_OFF), + zone2_power_on("", SET, ON_OFF), + zone2_volume("", SET, DIMENSIONLESS_DECIBEL), + zone2_set_volume("", NUMBER, STRING), + zone2_input("", NONE, STRING), + zone1_band("", TOGGLE, STRING), + band_am("", SET, STRING), + band_fm("", SET, STRING), + zone2_mute("", TOGGLE, ON_OFF), + zone2_mute_off("", SET, ON_OFF), + zone2_mute_on("", SET, ON_OFF), + zone2_band("", SET, NOT_IMPLEMENTED), + frequency("", UP_DOWN_SINGLE, ON_OFF), + seek("", UP_DOWN_SINGLE, ON_OFF), + channel("", UP_DOWN_SINGLE, ON_OFF), + stereo("", SET, STRING), + direct("", SET, STRING), + dolby("", SET, STRING), + dts("", SET, STRING), + all_stereo("", SET, STRING), + auto("", SET, STRING), + reference_stereo("", SET, STRING), + surround_mode("", SET, STRING), + preset1("Preset 1", SET, STRING), + preset2("Preset 2", SET, STRING), + dirac("Dirac", SET, STRING), + hdmi1("HDMI 1", SOURCE_MAIN_ZONE, STRING), + hdmi2("HDMI 2", SOURCE_MAIN_ZONE, STRING), + hdmi3("HDMI 3", SOURCE_MAIN_ZONE, STRING), + hdmi4("HDMI 4", SOURCE_MAIN_ZONE, STRING), + hdmi5("HDMI 5", SOURCE_MAIN_ZONE, STRING), + hdmi6("HDMI 6", SOURCE_MAIN_ZONE, STRING), + hdmi7("HDMI 7", SOURCE_MAIN_ZONE, STRING), + hdmi8("HDMI 8", SOURCE_MAIN_ZONE, STRING), + analog1("Analog 1", SOURCE_MAIN_ZONE, STRING), + analog2("Analog 2", SOURCE_MAIN_ZONE, STRING), + analog3("Analog 3", SOURCE_MAIN_ZONE, STRING), + analog4("Analog 4", SOURCE_MAIN_ZONE, STRING), + analog5("Analog 5", SOURCE_MAIN_ZONE, STRING), + analog71("Analog 7.1", SOURCE_MAIN_ZONE, STRING), + ARC("Audio Return Channel", SOURCE_MAIN_ZONE, STRING), + coax1("Coax 1", SOURCE_MAIN_ZONE, STRING), + coax2("Coax 2", SOURCE_MAIN_ZONE, STRING), + coax3("Coax 3", SOURCE_MAIN_ZONE, STRING), + coax4("Coax 4", SOURCE_MAIN_ZONE, STRING), + front_in("Front In", SOURCE_MAIN_ZONE, STRING), + optical1("Optical 1", SOURCE_MAIN_ZONE, STRING), + optical2("Optical 2", SOURCE_MAIN_ZONE, STRING), + optical3("Optical 3", SOURCE_MAIN_ZONE, STRING), + optical4("Optical 4", SOURCE_MAIN_ZONE, STRING), + tuner("Tuner 1", SOURCE_MAIN_ZONE, STRING), + usb_stream("USB Stream", SOURCE_MAIN_ZONE, STRING), + center_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL), + subwoofer_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL), + surround_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL), + back_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL), + width_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL), + height_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL), + zone2_analog1("Analog 1", SOURCE_ZONE2, STRING), + zone2_analog2("Analog 2", SOURCE_ZONE2, STRING), + zone2_analog3("Analog 3", SOURCE_ZONE2, STRING), + zone2_analog4("Analog 4", SOURCE_ZONE2, STRING), + zone2_analog5("Analog 5", SOURCE_ZONE2, STRING), + zone2_analog71("Analog 7.1", SOURCE_ZONE2, STRING), + zone2_analog8("Analog 8", SOURCE_ZONE2, STRING), + zone2_ARC("Audio Return Channel", SOURCE_ZONE2, STRING), + zone2_coax1("Coax 1", SOURCE_ZONE2, STRING), + zone2_coax2("Coax 2", SOURCE_ZONE2, STRING), + zone2_coax3("Coax 3", SOURCE_ZONE2, STRING), + zone2_coax4("Coax 4", SOURCE_ZONE2, STRING), + zone2_ethernet("Ethernet", SOURCE_ZONE2, STRING), + zone2_follow_main("Follow Main", SOURCE_ZONE2, STRING), + zone2_front_in("Front In", SOURCE_ZONE2, STRING), + zone2_optical1("Optical 1", SOURCE_ZONE2, STRING), + zone2_optical2("Optical 2", SOURCE_ZONE2, STRING), + zone2_optical3("Optical 3", SOURCE_ZONE2, STRING), + zone2_optical4("Optical 4", SOURCE_ZONE2, STRING), + channel_1("Channel 1", SET, STRING), + channel_2("Channel 2", SET, STRING), + channel_3("Channel 3", SET, STRING), + channel_4("Channel 4", SET, STRING), + channel_5("Channel 5", SET, STRING), + channel_6("Channel 6", SET, STRING), + channel_7("Channel 7", SET, STRING), + channel_8("Channel 8", SET, STRING), + channel_9("Channel 9", SET, STRING), + channel_10("Channel 10", SET, STRING), + channel_11("Channel 11", SET, STRING), + channel_12("Channel 12", SET, STRING), + channel_13("Channel 13", SET, STRING), + channel_14("Channel 14", SET, STRING), + channel_15("Channel 15", SET, STRING), + channel_16("Channel 16", SET, STRING), + channel_17("Channel 17", SET, STRING), + channel_18("Channel 18", SET, STRING), + channel_19("Channel 19", SET, STRING), + channel_20("Channel 20", SET, STRING); + + private final String label; + private final EmotivaCommandType commandType; + private final EmotivaDataType dataType; + + EmotivaControlCommands(String label, EmotivaCommandType commandType, EmotivaDataType dataType) { + this.label = label; + this.commandType = commandType; + this.dataType = dataType; + } + + public static EmotivaControlCommands matchToInput(String inputName) { + for (EmotivaControlCommands value : values()) { + if (inputName.toLowerCase().equals(value.name())) { + return value; + } + } + if (inputName.startsWith("input_")) { + return valueOf(inputName.replace("input_", "source_")); + } + return none; + } + + public String getLabel() { + return label; + } + + public EmotivaCommandType getCommandType() { + return commandType; + } + + public EmotivaDataType getDataType() { + return dataType; + } + + public static EnumMap getCommandsFromType(EmotivaCommandType filter) { + EnumMap commands = new EnumMap<>(EmotivaControlCommands.class); + for (EmotivaControlCommands value : values()) { + if (value.getCommandType().equals(filter)) { + StringBuilder sb = new StringBuilder(value.name()); + sb.setCharAt(0, Character.toUpperCase(value.name().charAt(0))); + commands.put(value, sb.toString()); + } + } + return commands; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequest.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequest.java new file mode 100644 index 00000000000..ab529c742b4 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequest.java @@ -0,0 +1,475 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.clamp; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumePercentageToDecibel; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.*; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.FREQUENCY_HERTZ; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_band; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_channel; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Binds channels to a given command with datatype. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class EmotivaControlRequest { + private final Logger logger = LoggerFactory.getLogger(EmotivaControlRequest.class); + private String name; + private final EmotivaDataType dataType; + private String channel; + private final EmotivaControlCommands defaultCommand; + private final EmotivaControlCommands setCommand; + private final EmotivaControlCommands onCommand; + private final EmotivaControlCommands offCommand; + private final EmotivaControlCommands upCommand; + private final EmotivaControlCommands downCommand; + private double maxValue; + private double minValue; + private final Map> commandMaps; + private final EmotivaProtocolVersion protocolVersion; + + public EmotivaControlRequest(String channel, EmotivaSubscriptionTags channelSubscription, + EmotivaControlCommands controlCommand, Map> commandMaps, + EmotivaProtocolVersion protocolVersion) { + if (channelSubscription.equals(EmotivaSubscriptionTags.unknown)) { + if (controlCommand.equals(EmotivaControlCommands.none)) { + this.defaultCommand = EmotivaControlCommands.none; + this.onCommand = EmotivaControlCommands.none; + this.offCommand = EmotivaControlCommands.none; + this.setCommand = EmotivaControlCommands.none; + this.upCommand = EmotivaControlCommands.none; + this.downCommand = EmotivaControlCommands.none; + } else { + this.defaultCommand = controlCommand; + this.onCommand = resolveOnCommand(controlCommand); + this.offCommand = resolveOffCommand(controlCommand); + this.setCommand = resolveSetCommand(controlCommand); + this.upCommand = resolveUpCommand(controlCommand); + this.downCommand = resolveDownCommand(controlCommand); + } + } else { + this.defaultCommand = resolveControlCommand(channelSubscription.getEmotivaName(), controlCommand); + if (controlCommand.equals(EmotivaControlCommands.none)) { + this.onCommand = resolveOnCommand(defaultCommand); + this.offCommand = resolveOffCommand(defaultCommand); + this.setCommand = resolveSetCommand(defaultCommand); + this.upCommand = resolveUpCommand(defaultCommand); + this.downCommand = resolveDownCommand(defaultCommand); + } else { + this.onCommand = controlCommand; + this.offCommand = controlCommand; + this.setCommand = controlCommand; + this.upCommand = controlCommand; + this.downCommand = controlCommand; + } + } + this.name = defaultCommand.name(); + this.dataType = defaultCommand.getDataType(); + this.channel = channel; + this.commandMaps = commandMaps; + this.protocolVersion = protocolVersion; + if (name.equals(EmotivaControlCommands.volume.name()) + || name.equals(EmotivaControlCommands.zone2_volume.name())) { + minValue = DEFAULT_VOLUME_MIN_DECIBEL; + maxValue = DEFAULT_VOLUME_MAX_DECIBEL; + } else if (setCommand.name().endsWith(TRIM_SET_COMMAND_SUFFIX)) { + minValue = DEFAULT_TRIM_MIN_DECIBEL * 2; + maxValue = DEFAULT_TRIM_MAX_DECIBEL * 2; + } + } + + public EmotivaControlDTO createDTO(Command ohCommand, @Nullable State previousState) { + switch (defaultCommand.getCommandType()) { + case CYCLE -> { + return EmotivaControlDTO.create(defaultCommand); + } + case MENU_CONTROL -> { + if (ohCommand instanceof StringType value) { + try { + return EmotivaControlDTO.create(EmotivaControlCommands.valueOf(value.toString().toLowerCase())); + } catch (IllegalArgumentException e) { + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } + } + case MODE -> { + if (ohCommand instanceof StringType value) { + // Check if value can be interpreted as a mode- + try { + OHChannelToEmotivaCommand ohChannelToEmotivaCommand = OHChannelToEmotivaCommand + .valueOf(value.toString()); + return EmotivaControlDTO.create(ohChannelToEmotivaCommand.getCommand()); + } catch (IllegalArgumentException e) { + if ("1".equals(value.toString())) { + return EmotivaControlDTO.create(getUpCommand(), 1); + } else if ("-1".equals(value.toString())) { + return EmotivaControlDTO.create(getDownCommand(), -1); + } + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } else if (ohCommand instanceof Number value) { + if (value.intValue() >= 1) { + return EmotivaControlDTO.create(getUpCommand(), 1); + } else if (value.intValue() <= -1) { + return EmotivaControlDTO.create(getDownCommand(), -1); + } + } + } + case NUMBER -> { + if (ohCommand instanceof Number value) { + return handleNumberTypes(getSetCommand(), ohCommand, value); + } else { + logger.debug("Could not create EmotivaControlDTO for {}:{}:{}, ohCommand is {}", channel, name, + NUMBER, ohCommand.getClass().getSimpleName()); + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } + case NONE -> { + switch (channel) { + case CHANNEL_TUNER_BAND -> { + return matchToCommandMap(ohCommand, tuner_band.getEmotivaName()); + } + case CHANNEL_TUNER_CHANNEL_SELECT -> { + return matchToCommandMap(ohCommand, tuner_channel.getEmotivaName()); + } + case CHANNEL_SOURCE -> { + return matchToCommandMap(ohCommand, MAP_SOURCES_MAIN_ZONE); + } + case CHANNEL_ZONE2_SOURCE -> { + return matchToCommandMap(ohCommand, MAP_SOURCES_ZONE_2); + } + default -> { + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } + } + case SET -> { + if (ohCommand instanceof StringType value) { + return EmotivaControlDTO.create(getSetCommand(), value.toString()); + } else if (ohCommand instanceof Number value) { + return handleNumberTypes(getSetCommand(), ohCommand, value); + } else if (ohCommand instanceof OnOffType value) { + if (value.equals(OnOffType.ON)) { + return EmotivaControlDTO.create(getOnCommand()); + } else { + return EmotivaControlDTO.create(getOffCommand()); + } + } else { + logger.debug("Could not create EmotivaControlDTO for {}:{}:{}, ohCommand is {}", channel, name, SET, + ohCommand.getClass().getSimpleName()); + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } + case SPEAKER_PRESET -> { + if (ohCommand instanceof StringType value) { + try { + return EmotivaControlDTO.create(EmotivaControlCommands.valueOf(value.toString())); + } catch (IllegalArgumentException e) { + // No match found for preset command, default to cycling + return EmotivaControlDTO.create(defaultCommand); + } + } else { + return EmotivaControlDTO.create(defaultCommand); + } + } + case TOGGLE -> { + if (ohCommand instanceof OnOffType value) { + if (value.equals(OnOffType.ON)) { + return EmotivaControlDTO.create(getOnCommand()); + } else { + return EmotivaControlDTO.create(getOffCommand()); + } + } else { + logger.debug("Could not create EmotivaControlDTO for {}:{}:{}, ohCommand is {}", channel, name, + TOGGLE, ohCommand.getClass().getSimpleName()); + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } + case UP_DOWN_SINGLE -> { + if (ohCommand instanceof Number value) { + if (dataType.equals(FREQUENCY_HERTZ)) { + if (previousState instanceof Number pre) { + if (value.doubleValue() > pre.doubleValue()) { + return EmotivaControlDTO.create(getUpCommand(), 1); + } else if (value.doubleValue() < pre.doubleValue()) { + return EmotivaControlDTO.create(getDownCommand(), -1); + } + } + } + if (value.intValue() <= maxValue || value.intValue() >= minValue) { + if (value.intValue() >= 1) { + return EmotivaControlDTO.create(getUpCommand(), 1); + } else if (value.intValue() <= -1) { + return EmotivaControlDTO.create(getDownCommand(), -1); + } + } + // Reached max or min value, not sending anything + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } else if (ohCommand instanceof StringType value) { + if ("1".equals(value.toString())) { + return EmotivaControlDTO.create(getUpCommand(), 1); + } else if ("-1".equals(value.toString())) { + return EmotivaControlDTO.create(getDownCommand(), -1); + } + } else if (ohCommand instanceof UpDownType value) { + if (value.equals(UpDownType.UP)) { + return EmotivaControlDTO.create(getUpCommand(), 1); + } else { + return EmotivaControlDTO.create(getDownCommand(), -1); + } + } else { + logger.debug("Could not create EmotivaControlDTO for {}:{}:{}, ohCommand is {}", channel, name, + UP_DOWN_SINGLE, ohCommand.getClass().getSimpleName()); + } + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + case UP_DOWN_HALF -> { + if (ohCommand instanceof Number value) { + if (value.intValue() <= maxValue || value.intValue() >= minValue) { + Number pre = (Number) previousState; + if (pre == null) { + if (value.doubleValue() > 0) { + return EmotivaControlDTO.create(getUpCommand()); + } else if (value.doubleValue() < 0) { + return EmotivaControlDTO.create(getDownCommand()); + } + } else { + if (value.doubleValue() > pre.doubleValue()) { + return EmotivaControlDTO.create(getUpCommand()); + } else if (value.doubleValue() < pre.doubleValue()) { + return EmotivaControlDTO.create(getDownCommand()); + } + } + } + } else { + logger.debug("Could not create EmotivaControlDTO for {}:{}:{}, ohCommand is {}", channel, name, + UP_DOWN_HALF, ohCommand.getClass().getSimpleName()); + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } + default -> { + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + + private EmotivaControlDTO matchToCommandMap(Command ohCommand, String mapName) { + if (ohCommand instanceof StringType value) { + Map commandMap = commandMaps.get(mapName); + if (commandMap != null) { + for (EmotivaControlCommands command : commandMap.keySet()) { + String map = commandMap.get(command); + if (map != null && map.equals(value.toString())) { + return EmotivaControlDTO.create(EmotivaControlCommands.matchToInput(command.toString())); + } else if (command.name().equalsIgnoreCase(value.toString())) { + return EmotivaControlDTO.create(command); + } + } + } + } + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + + private EmotivaControlDTO handleNumberTypes(EmotivaControlCommands setCommand, Command ohCommand, Number value) { + switch (dataType) { + case DIMENSIONLESS_PERCENT -> { + if (name.equals(EmotivaControlCommands.volume.name())) { + return EmotivaControlDTO.create(EmotivaControlCommands.set_volume, + volumePercentageToDecibel(value.intValue())); + } else if (name.equals(EmotivaControlCommands.zone2_set_volume.name())) { + return EmotivaControlDTO.create(EmotivaControlCommands.zone2_set_volume, + volumePercentageToDecibel(value.intValue())); + } else { + return EmotivaControlDTO.create(setCommand, value.intValue()); + } + } + case DIMENSIONLESS_DECIBEL -> { + if (name.equals(EmotivaControlCommands.volume.name())) { + return createForVolumeSetCommand(ohCommand, value, EmotivaControlCommands.set_volume); + } else if (name.equals(EmotivaControlCommands.zone2_volume.name())) { + return createForVolumeSetCommand(ohCommand, value, EmotivaControlCommands.zone2_set_volume); + } else { + double doubleValue = setCommand.name().endsWith(TRIM_SET_COMMAND_SUFFIX) + ? value.doubleValue() * PROTOCOL_V3_LEVEL_MULTIPLIER + : value.doubleValue(); + if (doubleValue >= maxValue) { + return EmotivaControlDTO.create(getSetCommand(), maxValue); + } else if (doubleValue <= minValue) { + return EmotivaControlDTO.create(getSetCommand(), minValue); + } else { + return EmotivaControlDTO.create(getSetCommand(), doubleValue); + } + } + } + case FREQUENCY_HERTZ -> { + return EmotivaControlDTO.create(getDefaultCommand(), value.intValue()); + } + default -> { + logger.debug("Could not create EmotivaControlDTO for {}:{}:{}, ohCommand is {}", channel, name, + setCommand.getDataType(), ohCommand.getClass().getSimpleName()); + return EmotivaControlDTO.create(EmotivaControlCommands.none); + } + } + } + + private EmotivaControlDTO createForVolumeSetCommand(Command ohCommand, Number value, + EmotivaControlCommands emotivaControlCommands) { + if (ohCommand instanceof PercentType) { + return EmotivaControlDTO.create(emotivaControlCommands, volumePercentageToDecibel(value.intValue())); + } else { + return EmotivaControlDTO.create(emotivaControlCommands, clamp(value, minValue, maxValue)); + } + } + + private EmotivaControlCommands resolveUpCommand(EmotivaControlCommands controlCommand) { + try { + return EmotivaControlCommands.valueOf("%s_up".formatted(controlCommand.name())); + } catch (IllegalArgumentException e) { + // not found, setting original command + return controlCommand; + } + } + + private EmotivaControlCommands resolveDownCommand(EmotivaControlCommands controlCommand) { + try { + return EmotivaControlCommands.valueOf("%s_down".formatted(controlCommand.name())); + } catch (IllegalArgumentException e) { + // not found, setting original command + return controlCommand; + } + } + + private EmotivaControlCommands resolveControlCommand(String name, EmotivaControlCommands controlCommand) { + try { + return controlCommand.equals(EmotivaControlCommands.none) ? EmotivaControlCommands.valueOf(name) + : controlCommand; + } catch (IllegalArgumentException e) { + // ignore + } + return EmotivaControlCommands.none; + } + + private EmotivaControlCommands resolveOnCommand(EmotivaControlCommands controlCommand) { + try { + return EmotivaControlCommands.valueOf("%s_on".formatted(controlCommand.name())); + } catch (IllegalArgumentException e) { + // not found, setting original command + return controlCommand; + } + } + + private EmotivaControlCommands resolveOffCommand(EmotivaControlCommands controlCommand) { + try { + return EmotivaControlCommands.valueOf("%s_off".formatted(controlCommand.name())); + } catch (IllegalArgumentException e) { + // not found, using original command + return controlCommand; + } + } + + /** + * Checks for commands with _trim_set suffix, which indicate speaker trims with a fixed min/max value. + */ + private EmotivaControlCommands resolveSetCommand(EmotivaControlCommands controlCommand) { + try { + return EmotivaControlCommands.valueOf("%s_trim_set".formatted(controlCommand.name())); + } catch (IllegalArgumentException e) { + // not found, using original command + return controlCommand; + } + } + + public String getName() { + return name; + } + + public EmotivaDataType getDataType() { + return dataType; + } + + public String getChannel() { + return channel; + } + + public EmotivaControlCommands getDefaultCommand() { + return defaultCommand; + } + + public void setName(String name) { + this.name = name; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public EmotivaControlCommands getSetCommand() { + return setCommand; + } + + public EmotivaControlCommands getOnCommand() { + return onCommand; + } + + public EmotivaControlCommands getOffCommand() { + return offCommand; + } + + public EmotivaControlCommands getUpCommand() { + return upCommand; + } + + public EmotivaControlCommands getDownCommand() { + return downCommand; + } + + public double getMaxValue() { + return maxValue; + } + + public double getMinValue() { + return minValue; + } + + public EmotivaProtocolVersion getProtocolVersion() { + return protocolVersion; + } + + @Override + public String toString() { + return "EmotivaControlRequest{" + "name='" + name + '\'' + ", dataType=" + dataType + ", channel='" + channel + + '\'' + ", defaultCommand=" + defaultCommand + ", setCommand=" + setCommand + ", onCommand=" + + onCommand + ", offCommand=" + offCommand + ", upCommand=" + upCommand + ", downCommand=" + downCommand + + ", maxValue=" + maxValue + ", minValue=" + minValue + ", commandMaps=" + commandMaps + + ", protocolVersion=" + protocolVersion + '}'; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaDataType.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaDataType.java new file mode 100644 index 00000000000..4a29b0dfceb --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaDataType.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This enum is used to describe the value types from Emotiva. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum EmotivaDataType { + DIMENSIONLESS_DECIBEL("decibel"), + DIMENSIONLESS_PERCENT("percent"), + FREQUENCY_HERTZ("hertz"), + NUMBER("number"), + NUMBER_TIME("number_time"), + GOODBYE("goodbye"), + NOT_IMPLEMENTED("not_implemented"), + ON_OFF("boolean"), + STRING("string"), + UNKNOWN("unknown"); + + private final String name; + + EmotivaDataType(String name) { + this.name = name; + } + + public static EmotivaDataType fromName(String name) { + EmotivaDataType result = EmotivaDataType.UNKNOWN; + for (EmotivaDataType m : EmotivaDataType.values()) { + if (m.name.equals(name)) { + result = m; + break; + } + } + return result; + } + + @Override + public String toString() { + return name; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaPropertyStatus.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaPropertyStatus.java new file mode 100644 index 00000000000..77f99e2a40e --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaPropertyStatus.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Status types for status fields of different message type. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum EmotivaPropertyStatus { + + VALID("ack"), + NOT_VALID("nak"); + + private final String value; + + EmotivaPropertyStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaProtocolVersion.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaProtocolVersion.java new file mode 100644 index 00000000000..feb590df249 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaProtocolVersion.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Enum for mapping Emotiva Network Protocol versions. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum EmotivaProtocolVersion { + + PROTOCOL_V2("2.0"), + PROTOCOL_V3("3.0"); + + private final String protocolVersion; + + EmotivaProtocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public static EmotivaProtocolVersion protocolFromConfig(String protocolVersion) { + for (EmotivaProtocolVersion value : values()) { + if (protocolVersion.equals(value.protocolVersion)) { + return value; + } + } + return PROTOCOL_V2; + } + + public String value() { + return protocolVersion; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaSubscriptionTags.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaSubscriptionTags.java new file mode 100644 index 00000000000..2e2d64809d0 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaSubscriptionTags.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.*; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Emotiva subscription tags with corresponding UoM data type and channel. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum EmotivaSubscriptionTags { + + /* Protocol V1 notify tags */ + power("power", ON_OFF, CHANNEL_MAIN_ZONE_POWER), + source("source", STRING, CHANNEL_SOURCE), + dim("dim", DIMENSIONLESS_PERCENT, CHANNEL_DIM), + mode("mode", STRING, CHANNEL_MODE), + speaker_preset("speaker-preset", STRING, CHANNEL_SPEAKER_PRESET), + center("center", DIMENSIONLESS_DECIBEL, CHANNEL_CENTER), + subwoofer("subwoofer", DIMENSIONLESS_DECIBEL, CHANNEL_SUBWOOFER), + surround("surround", DIMENSIONLESS_DECIBEL, CHANNEL_SURROUND), + back("back", DIMENSIONLESS_DECIBEL, CHANNEL_BACK), + volume("volume", DIMENSIONLESS_DECIBEL, CHANNEL_MAIN_VOLUME), + loudness("loudness", ON_OFF, CHANNEL_LOUDNESS), + treble("treble", DIMENSIONLESS_DECIBEL, CHANNEL_TREBLE), + bass("bass", DIMENSIONLESS_DECIBEL, CHANNEL_BASS), + zone2_power("zone2-power", ON_OFF, CHANNEL_ZONE2_POWER), + zone2_volume("zone2-volume", DIMENSIONLESS_DECIBEL, CHANNEL_ZONE2_VOLUME), + zone2_input("zone2-input", STRING, CHANNEL_ZONE2_SOURCE), + tuner_band("tuner-band", STRING, CHANNEL_TUNER_BAND), + tuner_channel("tuner-channel", FREQUENCY_HERTZ, CHANNEL_TUNER_CHANNEL), + tuner_signal("tuner-signal", STRING, CHANNEL_TUNER_SIGNAL), + tuner_program("tuner-program", STRING, CHANNEL_TUNER_PROGRAM), + tuner_RDS("tuner-RDS", STRING, CHANNEL_TUNER_RDS), + audio_input("audio-input", STRING, CHANNEL_AUDIO_INPUT), + audio_bitstream("audio-bitstream", STRING, CHANNEL_AUDIO_BITSTREAM), + audio_bits("audio-bits", STRING, CHANNEL_AUDIO_BITS), + video_input("video-input", STRING, CHANNEL_VIDEO_INPUT), + video_format("video-format", STRING, CHANNEL_VIDEO_FORMAT), + video_space("video-space", STRING, CHANNEL_VIDEO_SPACE), + input_1("input-1", STRING, CHANNEL_INPUT1), + input_2("input-2", STRING, CHANNEL_INPUT2), + input_3("input-3", STRING, CHANNEL_INPUT3), + input_4("input-4", STRING, CHANNEL_INPUT4), + input_5("input-5", STRING, CHANNEL_INPUT5), + input_6("input-6", STRING, CHANNEL_INPUT6), + input_7("input-7", STRING, CHANNEL_INPUT7), + input_8("input-8", STRING, CHANNEL_INPUT8), + + /* Protocol V2 notify tags */ + selected_mode("selected-mode", STRING, CHANNEL_SELECTED_MODE), + selected_movie_music("selected-movie-music", STRING, CHANNEL_SELECTED_MOVIE_MUSIC), + mode_ref_stereo("mode-ref-stereo", STRING, CHANNEL_MODE_REF_STEREO), + mode_stereo("mode-stereo", STRING, CHANNEL_MODE_STEREO), + mode_music("mode-music", STRING, CHANNEL_MODE_MUSIC), + mode_movie("mode-movie", STRING, CHANNEL_MODE_MOVIE), + mode_direct("mode-direct", STRING, CHANNEL_MODE_DIRECT), + mode_dolby("mode-dolby", STRING, CHANNEL_MODE_DOLBY), + mode_dts("mode-dts", STRING, CHANNEL_MODE_DTS), + mode_all_stereo("mode-all-stereo", STRING, CHANNEL_MODE_ALL_STEREO), + mode_auto("mode-auto", STRING, CHANNEL_MODE_AUTO), + mode_surround("mode-surround", STRING, CHANNEL_MODE_SURROUND), + menu("menu", ON_OFF, CHANNEL_MENU), + menu_update("menu-update", STRING, CHANNEL_MENU_DISPLAY_PREFIX), + + /* Protocol V3 notify tags */ + keepAlive("keepAlive", NUMBER_TIME, LAST_SEEN_STATE_NAME), + goodBye("goodBye", GOODBYE, ""), + bar_update("bar-update", STRING, CHANNEL_BAR), + width("width", DIMENSIONLESS_DECIBEL, CHANNEL_WIDTH), + height("height", DIMENSIONLESS_DECIBEL, CHANNEL_HEIGHT), + + /* Notify tag not in the documentation */ + source_tuner("source-tuner", ON_OFF, ""), + + /* No match tag */ + unknown("unknown", UNKNOWN, ""); + + private final Logger logger = LoggerFactory.getLogger(EmotivaSubscriptionTags.class); + + /* For error handling */ + public static final String UNKNOWN_TAG = "unknown"; + + private final String name; + private final EmotivaDataType dataType; + private final String channel; + + EmotivaSubscriptionTags(String name, EmotivaDataType dataType, String channel) { + this.name = name; + this.dataType = dataType; + this.channel = channel; + } + + public static boolean hasChannel(String name) { + try { + EmotivaSubscriptionTags type = EmotivaSubscriptionTags.valueOf(name); + if (!type.channel.isEmpty()) { + return true; + } + } catch (IllegalArgumentException e) { + // do nothing + } + return false; + } + + public static EmotivaSubscriptionTags fromChannelUID(String id) { + for (EmotivaSubscriptionTags value : values()) { + if (id.equals(value.getChannel())) { + return value; + } + } + return EmotivaSubscriptionTags.unknown; + } + + public static EmotivaSubscriptionTags[] generalChannels() { + List tags = new ArrayList<>(); + for (EmotivaSubscriptionTags value : values()) { + if (value.channel.startsWith("general")) { + tags.add(value); + } + } + return tags.toArray(new EmotivaSubscriptionTags[0]); + } + + public static EmotivaSubscriptionTags[] nonGeneralChannels() { + List tags = new ArrayList<>(); + for (EmotivaSubscriptionTags value : values()) { + if (!value.channel.startsWith("general")) { + tags.add(value); + } + } + return tags.toArray(new EmotivaSubscriptionTags[0]); + } + + public static EmotivaSubscriptionTags[] speakerChannels() { + List tags = new ArrayList<>(); + for (EmotivaSubscriptionTags value : values()) { + if (value.getDataType().equals(DIMENSIONLESS_DECIBEL)) { + tags.add(value); + } + } + return tags.toArray(new EmotivaSubscriptionTags[0]); + } + + public static List noSubscriptionToChannel() { + return List.of(goodBye); + } + + public String getName() { + return name; + } + + public String getEmotivaName() { + String retVal = name.replaceAll("-", "_"); + logger.debug("Converting OH channel '{}' to Emotiva command '{}'", name, retVal); + return retVal; + } + + public EmotivaDataType getDataType() { + return dataType; + } + + public String getChannel() { + return channel; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaUdpResponse.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaUdpResponse.java new file mode 100644 index 00000000000..0027c9fab00 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaUdpResponse.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +/** + * The class {@link EmotivaUdpResponse} represents UDP response we expect. + * + * @author Andi Bräu - Initial contribution + * @author Espen Fossen - Adpated to Emotiva binding + */ +public record EmotivaUdpResponse(String answer, String ipAddress) { + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EmotivaUdpResponse that = (EmotivaUdpResponse) o; + return answer.equals(that.answer) && ipAddress.equals(that.ipAddress); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtils.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtils.java new file mode 100644 index 00000000000..abbc66ca6d0 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtils.java @@ -0,0 +1,298 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.UNKNOWN_TAG; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.stream.StreamSource; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.emotiva.internal.dto.AbstractJAXBElementDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaAckDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaBarNotifyDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaBarNotifyWrapper; +import org.openhab.binding.emotiva.internal.dto.EmotivaCommandDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaMenuNotifyDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyWrapper; +import org.openhab.binding.emotiva.internal.dto.EmotivaPingDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaPropertyDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionRequest; +import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionResponse; +import org.openhab.binding.emotiva.internal.dto.EmotivaTransponderDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaUnsubscribeDTO; +import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateRequest; +import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * Helper class for marshalling and unmarshalling Emotiva message types. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class EmotivaXmlUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(EmotivaXmlUtils.class); + Marshaller marshaller; + + JAXBContext context; + + public EmotivaXmlUtils() throws JAXBException { + context = JAXBContext.newInstance(EmotivaAckDTO.class, EmotivaBarNotifyWrapper.class, EmotivaBarNotifyDTO.class, + EmotivaCommandDTO.class, EmotivaControlDTO.class, EmotivaMenuNotifyDTO.class, + EmotivaNotifyWrapper.class, EmotivaPingDTO.class, EmotivaPropertyDTO.class, + EmotivaSubscriptionRequest.class, EmotivaSubscriptionResponse.class, EmotivaTransponderDTO.class, + EmotivaUnsubscribeDTO.class, EmotivaUpdateRequest.class, EmotivaUpdateResponse.class); + marshaller = context.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + } + + public String marshallEmotivaDTO(Object objectInstanceType) { + try { + StringWriter out = new StringWriter(); + marshaller.marshal(objectInstanceType, out); + return out.toString(); + } catch (JAXBException e) { + LOGGER.debug("Could not marshall class of type {}", objectInstanceType.getClass().getName(), e); + } + return ""; + } + + public String marshallJAXBElementObjects(AbstractJAXBElementDTO jaxbElementDTO) { + try { + StringWriter out = new StringWriter(); + + List> commandsAsJAXBElement = new ArrayList<>(); + + if (jaxbElementDTO.getCommands() != null) { + for (EmotivaCommandDTO command : jaxbElementDTO.getCommands()) { + if (command.getName() != null) { + StringBuilder sb = new StringBuilder(); + if (command.getValue() != null) { + sb.append(" value=\"").append(command.getValue()).append("\""); + } + if (command.getStatus() != null) { + sb.append(" status=\"").append(command.getStatus()).append("\""); + } + if (command.getVisible() != null) { + sb.append(" visible=\"").append(command.getVisible()).append("\""); + } + if (command.getAck() != null) { + sb.append(" ack=\"").append(command.getAck()).append("\""); + } + QName name = new QName("%s%s".formatted(command.getName().trim(), sb)); + commandsAsJAXBElement.add(jaxbElementDTO.createJAXBElement(name)); + } + } + } + + // Replace commands with modified JaxbElements for Emotiva compatible marshalling + jaxbElementDTO.setJaxbElements(commandsAsJAXBElement); + jaxbElementDTO.setCommands(Collections.emptyList()); + + marshaller.marshal(jaxbElementDTO, out); + + // Remove JAXB added xsi and xmlns data, not needed + return out.toString().replaceAll("xsi:nil=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", + ""); + } catch (JAXBException e) { + LOGGER.debug("Could not marshall class of type {}", jaxbElementDTO.getClass().getName(), e); + } + return ""; + } + + public Object unmarshallToEmotivaDTO(String xmlAsString) throws JAXBException { + Object object; + Unmarshaller unmarshaller = context.createUnmarshaller(); + + if (xmlAsString.isEmpty()) { + throw new JAXBException("Could not unmarshall value, xml value is null or empty"); + } + + StringReader xmlAsStringReader = new StringReader(xmlAsString); + StreamSource xmlAsStringStream = new StreamSource(xmlAsStringReader); + object = unmarshaller.unmarshal(xmlAsStringStream); + return object; + } + + public List unmarshallXmlObjectsToControlCommands(List objects) { + List commands = new ArrayList<>(); + for (Object object : objects) { + try { + Element xmlElement = (Element) object; + + try { + EmotivaCommandDTO commandDTO = getEmotivaCommandDTO(xmlElement); + commands.add(commandDTO); + } catch (IllegalArgumentException e) { + LOGGER.debug("Notify tag {} is unknown or not defined, skipping.", xmlElement.getTagName(), e); + } + } catch (ClassCastException e) { + LOGGER.debug("Could not cast object to Element, object is of type {}", object.getClass()); + } + } + return commands; + } + + public List unmarshallToNotification(List objects) { + List commands = new ArrayList<>(); + for (Object object : objects) { + try { + Element xmlElement = (Element) object; + + try { + EmotivaNotifyDTO tagDTO = getEmotivaNotifyTags(xmlElement); + commands.add(tagDTO); + } catch (IllegalArgumentException e) { + LOGGER.debug("Notify tag {} is unknown or not defined, skipping.", xmlElement.getTagName(), e); + } + } catch (ClassCastException e) { + LOGGER.debug("Could not cast object to Element, object is of type {}", object.getClass()); + } + } + return commands; + } + + public List unmarshallToBarNotify(List objects) { + List commands = new ArrayList<>(); + for (Object object : objects) { + try { + Element xmlElement = (Element) object; + + try { + EmotivaBarNotifyDTO tagDTO = getEmotivaBarNotify(xmlElement); + commands.add(tagDTO); + } catch (IllegalArgumentException e) { + LOGGER.debug("Bar notify type {} is unknown or not defined, skipping.", xmlElement.getTagName(), e); + } + } catch (ClassCastException e) { + LOGGER.debug("Could not cast object to Element, object is of type {}", object.getClass()); + } + } + return commands; + } + + public List unmarshallToCommands(String elementAsString) { + List commands = new ArrayList<>(); + try { + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = builderFactory.newDocumentBuilder(); + + String[] lines = elementAsString.split("\n"); + for (String line : lines) { + + if (line.trim().startsWith("<") && line.trim().endsWith("/>")) { + Document doc = db.parse(new ByteArrayInputStream(line.getBytes(StandardCharsets.UTF_8))); + doc.getDocumentElement(); + EmotivaCommandDTO commandDTO = getEmotivaCommandDTO(doc.getDocumentElement()); + commands.add(commandDTO); + } + } + } catch (SAXException | IOException | ParserConfigurationException e) { + LOGGER.debug("Error unmarshall elements to commands", e); + } + return commands; + } + + private static EmotivaCommandDTO getEmotivaCommandDTO(Element xmlElement) { + EmotivaControlCommands commandType; + try { + commandType = EmotivaControlCommands.valueOf(xmlElement.getTagName().trim()); + } catch (IllegalArgumentException e) { + LOGGER.debug("Could not create EmotivaCommand, unknown command {}", xmlElement.getTagName()); + commandType = EmotivaControlCommands.none; + } + EmotivaCommandDTO commandDTO = new EmotivaCommandDTO(commandType); + if (xmlElement.hasAttribute("status")) { + commandDTO.setStatus(xmlElement.getAttribute("status")); + } + if (xmlElement.hasAttribute("value")) { + commandDTO.setValue(xmlElement.getAttribute("value")); + } + if (xmlElement.hasAttribute("visible")) { + commandDTO.setVisible(xmlElement.getAttribute("visible")); + } + return commandDTO; + } + + private static EmotivaBarNotifyDTO getEmotivaBarNotify(Element xmlElement) { + EmotivaBarNotifyDTO barNotify = new EmotivaBarNotifyDTO(xmlElement.getTagName().trim()); + if (xmlElement.hasAttribute("type")) { + barNotify.setType(xmlElement.getAttribute("type")); + } + if (xmlElement.hasAttribute("text")) { + barNotify.setText(xmlElement.getAttribute("text")); + } + if (xmlElement.hasAttribute("units")) { + barNotify.setUnits(xmlElement.getAttribute("units")); + } + if (xmlElement.hasAttribute("value")) { + barNotify.setValue(xmlElement.getAttribute("value")); + } + if (xmlElement.hasAttribute("min")) { + barNotify.setMin(xmlElement.getAttribute("min")); + } + if (xmlElement.hasAttribute("max")) { + barNotify.setMax(xmlElement.getAttribute("max")); + } + return barNotify; + } + + private static EmotivaNotifyDTO getEmotivaNotifyTags(Element xmlElement) { + String notifyTagName; + try { + notifyTagName = EmotivaSubscriptionTags.valueOf(xmlElement.getTagName().trim()).name(); + } catch (IllegalArgumentException e) { + LOGGER.debug("Could not create EmotivaNotify, unknown subscription tag {}", xmlElement.getTagName()); + notifyTagName = UNKNOWN_TAG; + } + EmotivaNotifyDTO commandDTO = new EmotivaNotifyDTO(notifyTagName); + if (xmlElement.hasAttribute("status")) { + commandDTO.setStatus(xmlElement.getAttribute("status")); + } + if (xmlElement.hasAttribute("value")) { + commandDTO.setValue(xmlElement.getAttribute("value")); + } + if (xmlElement.hasAttribute("visible")) { + commandDTO.setVisible(xmlElement.getAttribute("visible")); + } + if (xmlElement.hasAttribute("ack")) { + commandDTO.setAck(xmlElement.getAttribute("ack")); + } + return commandDTO; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/OHChannelToEmotivaCommand.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/OHChannelToEmotivaCommand.java new file mode 100644 index 00000000000..51e4627db88 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/OHChannelToEmotivaCommand.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_CHANNEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_FREQUENCY; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_HEIGHT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MAIN_VOLUME; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MAIN_VOLUME_DB; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_CONTROL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_DOWN; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_ENTER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_LEFT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_RIGHT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_UP; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_ALL_STEREO; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_AUTO; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_DIRECT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_DOLBY; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_DTS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_MOVIE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_MUSIC; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_REF_STEREO; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_STEREO; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_SURROUND; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MUTE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SEEK; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SOURCE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_STANDBY; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SURROUND_MODE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_WIDTH; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_MUTE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_SOURCE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME_DB; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Maps OH channels with only an indirect connection to an Emotiva command. Only handles 1:1 mappings. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum OHChannelToEmotivaCommand { + + standby(CHANNEL_STANDBY, EmotivaControlCommands.standby), + source(CHANNEL_SOURCE, EmotivaControlCommands.input), + menu(CHANNEL_MENU, EmotivaControlCommands.menu), + menu_control(CHANNEL_MENU_CONTROL, EmotivaControlCommands.menu_control), + up(CHANNEL_MENU_UP, EmotivaControlCommands.up), + down(CHANNEL_MENU_DOWN, EmotivaControlCommands.down), + left(CHANNEL_MENU_LEFT, EmotivaControlCommands.left), + right(CHANNEL_MENU_RIGHT, EmotivaControlCommands.right), + enter(CHANNEL_MENU_ENTER, EmotivaControlCommands.enter), + mute(CHANNEL_MUTE, EmotivaControlCommands.mute), + volume(CHANNEL_MAIN_VOLUME, EmotivaControlCommands.volume), + volume_db(CHANNEL_MAIN_VOLUME_DB, EmotivaControlCommands.volume), + zone2_volume(CHANNEL_ZONE2_VOLUME, EmotivaControlCommands.zone2_volume), + zone2_volume_db(CHANNEL_ZONE2_VOLUME_DB, EmotivaControlCommands.zone2_volume), + zone2_mute(CHANNEL_ZONE2_MUTE, EmotivaControlCommands.zone2_mute), + zone2_source(CHANNEL_ZONE2_SOURCE, EmotivaControlCommands.zone2_input), + width(CHANNEL_WIDTH, EmotivaControlCommands.width_trim_set), + height(CHANNEL_HEIGHT, EmotivaControlCommands.height_trim_set), + frequency(CHANNEL_FREQUENCY, EmotivaControlCommands.frequency), + seek(CHANNEL_SEEK, EmotivaControlCommands.seek), + channel(CHANNEL_CHANNEL, EmotivaControlCommands.channel), + mode_ref_stereo(CHANNEL_MODE_REF_STEREO, EmotivaControlCommands.reference_stereo), + surround_mode(CHANNEL_SURROUND_MODE, EmotivaControlCommands.surround_mode), + mode_surround(CHANNEL_MODE_SURROUND, EmotivaControlCommands.surround_mode), + mode_stereo(CHANNEL_MODE_STEREO, EmotivaControlCommands.stereo), + mode_music(CHANNEL_MODE_MUSIC, EmotivaControlCommands.music), + mode_movie(CHANNEL_MODE_MOVIE, EmotivaControlCommands.movie), + mode_direct(CHANNEL_MODE_DIRECT, EmotivaControlCommands.direct), + mode_dolby(CHANNEL_MODE_DOLBY, EmotivaControlCommands.dolby), + mode_dts(CHANNEL_MODE_DTS, EmotivaControlCommands.dts), + mode_all_stereo(CHANNEL_MODE_ALL_STEREO, EmotivaControlCommands.all_stereo), + mode_auto(CHANNEL_MODE_AUTO, EmotivaControlCommands.auto); + + private final String ohChannel; + private final EmotivaControlCommands command; + + OHChannelToEmotivaCommand(String ohChannel, EmotivaControlCommands command) { + this.ohChannel = ohChannel; + this.command = command; + } + + public String getChannel() { + return ohChannel; + } + + public EmotivaControlCommands getCommand() { + return command; + } + + public static EmotivaControlCommands fromChannelUID(String id) { + for (OHChannelToEmotivaCommand value : values()) { + if (id.equals(value.ohChannel)) { + return value.command; + } + } + return EmotivaControlCommands.none; + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..3cec24350a3 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,31 @@ + + + + binding + Emotiva Binding + This is the binding for devices from the Emotiva Audio Corporation. + local + + + + ip + + + type + ipBroadcast + + + destPort + 7001 + + + timeoutMs + 1000 + + + + + + diff --git a/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 00000000000..c0477ae3df5 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,61 @@ + + + + + + network-address + + IP Network Address where Emotiva device can be Reached. + + + control-port + + Network address port for control (UDP) + 7002 + true + + + notify-port + + Network address port for notifications (UDP) + 7003 + true + + + info-port + + Network address port for info (UDP) + 7004 + true + + + setup-port + + Network address port for menu notify port (UDP) + 7005 + true + + + setup-port + + Network address port for setup port (TCP) + 7100 + true + + + protocol-revision + + Protocol version, only change if you know what your doing + true + + + + The time to wait between reconnection attempts (in minutes) + 2 + true + + + diff --git a/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/i18n/emotiva.properties b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/i18n/emotiva.properties new file mode 100644 index 00000000000..6d7389e9e86 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/i18n/emotiva.properties @@ -0,0 +1,201 @@ +addon.emotiva.name = Emotiva Binding +addon.emotiva.description = This is the binding for Emotiva Audio Corporation AV processors. + +# thing types + +thing-type.emotiva.processor.label = Processor +thing-type.emotiva.processor.description = Control a Emotiva AV Processor. +thing-type.emotiva.processor.group.main-zone.label = Main Zone Control +thing-type.emotiva.processor.group.main-zone.description = Channels for the main zone of this device. +thing-type.emotiva.processor.group.zone2.label = Zone 2 Control +thing-type.emotiva.processor.group.zone2.description = Channels for Zone 2 of this device. + +# thing types config + +thing-type.config.emotiva.config.ipAddress.label = IP address +thing-type.config.emotiva.config.ipAddress.description = IP address of the device +thing-type.config.emotiva.config.controlPort = Control Port +thing-type.config.emotiva.config.controlPort.description = UDP port to send commands to the device +thing-type.config.emotiva.config.notifyPort.label = Notify Port +thing-type.config.emotiva.config.notifyPort.description = UDP port to receive notifications from the device +thing-type.config.emotiva.config.infoPort.label = Info Port +thing-type.config.emotiva.config.infoPort.description = UDP port +thing-type.config.emotiva.config.setupPortTCP.label = Setup TCP Port +thing-type.config.emotiva.config.setupPortTCP.description = TCP port for remote setup +thing-type.config.emotiva.config.menuNotifyPort.label = Menu Notify Port +thing-type.config.emotiva.config.menuNotifyPort.description = UDP port to receive menu notifications from the device +thing-type.config.emotiva.config.protocolVersion.label = Emotiva Protocol Version +thing-type.config.emotiva.config.protocolVersion.description = Emotiva Network Remote Control protocol version +thing-type.config.emotiva.config.keepAlive.label = Keep Alive Notification +thing-type.config.emotiva.config.keepAlive.description = The interval, in milliseconds, at which the Emotiva Device will send a "keepAlive" notification + +# channel group types + +channel-group-type.emotiva.general.label = General Control +channel-group-type.emotiva.general.description = General channels for this device. +channel-group-type.emotiva.zone.label = Zone Control +channel-group-type.emotiva.zone.description = Channels for a zone of this device. + +# channel types + +channel-type.emotiva.audio-input.label = Audio Input +channel-type.emotiva.audio-input.description = Source for audio input +channel-type.emotiva.audio-bitstream.label = Audio Input Bitstream Type +channel-type.emotiva.audio-bitstream.description = Current audio bitstream, "PCM 2.0", "ATMOS", etc. +channel-type.emotiva.audio-bits.label = Audio Input Bits +channel-type.emotiva.audio-bits.description = Current audio input bits: "32kHZ 24bits", etc. +channel-type.emotiva.bar.label = Front Panel Bar +channel-type.emotiva.bar.description = Displays text from the front panel bar of the device +channel-type.emotiva.channel.label = Radio Tuner Channel +channel-type.emotiva.channel.description = Changes radio tuner channel a station at a time, up or down +channel-type.emotiva.frequency.label = Radio Tuner Frequency +channel-type.emotiva.frequency.description = Changes radio tuner frequency, up or down +channel-type.emotiva.dim.label = Front Panel Dimness +channel-type.emotiva.dim.description = Percentage of light on front panel +channel-type.emotiva.input-name.label = Input Name +channel-type.emotiva.input-name.description = User assigned name for input or mode +channel-type.emotiva.loudness.label = Loudness +channel-type.emotiva.loudness.description = Loudness ON/OFF +channel-type.emotiva.mainPower.label = Power +channel-type.emotiva.mainPower.description = Power ON/OFF the device +channel-type.emotiva.menu.label = Menu +channel-type.emotiva.menu.description = Controls the device menu +channel-type.emotiva.mode.label = Mode +channel-type.emotiva.mode.description = Sets main zone mode, "Stereo", "Direct", "Auto", etc. +channel-type.emotiva.mode-surround.label = Surround Mode +channel-type.emotiva.mode-surround.description = Select the surround mode for this zone of the device +channel-type.emotiva.mute.label = Mute +channel-type.emotiva.mute.description = Enable/Disable Mute on this zone of the device +channel-type.emotiva.seek.label = Radio Tuner Seek +channel-type.emotiva.seek.description = Enables seek of radio channel, up or down +channel-type.emotiva.selected-mode.label = Selected Mode +channel-type.emotiva.selected-mode.description = User selected mode for the main zone. An "Auto" value here might not mean the mode channel is in auto. +channel-type.emotiva.selected-mode.state.option.all-stereo = All Stereo +channel-type.emotiva.selected-mode.state.option.auto = Auto +channel-type.emotiva.selected-mode.state.option.direct = Direct +channel-type.emotiva.selected-mode.state.option.dolby = Dolby +channel-type.emotiva.selected-mode.state.option.dts = DTS +channel-type.emotiva.selected-mode.state.option.stereo = Stereo +channel-type.emotiva.selected-mode.state.option.surround = Surround +channel-type.emotiva.selected-mode.state.option.ref-stereo = Reference Stereo +channel-type.emotiva.selected-movie-music.label = Selected Movie Music +channel-type.emotiva.selected-movie-music.description = User-selected movie or music mode for main zone: "Movie" or "Music". +channel-type.emotiva.selected-movie-music.state.option.movie = Movie +channel-type.emotiva.selected-movie-music.state.option.music = Music +channel-type.emotiva.speaker-preset.label = Speaker Preset +channel-type.emotiva.speaker-preset.description = Speaker Preset Name +channel-type.emotiva.speaker-preset.state.option.preset-1 = Speaker Preset 1 +channel-type.emotiva.speaker-preset.state.option.preset-2 = Speaker Preset 2 +channel-type.emotiva.source.label = Input Source +channel-type.emotiva.source.description = Select the input source for this zone of the device +channel-type.emotiva.standby.label = Standby +channel-type.emotiva.standby.description = Set device in standby mode +channel-type.emotiva.tuner-band.label = Radio Tuner Band +channel-type.emotiva.tuner-band.description = Set radio tuner band, "AM" or "FM" +channel-type.emotiva.tuner-band.state.option.band-am = AM +channel-type.emotiva.tuner-band.state.option.band-fm = FM +channel-type.emotiva.tuner-channel.label = Radio Tuner Channel Frequency +channel-type.emotiva.tuner-channel.description = Frequency of user selected radio channel +channel-type.emotiva.tuner-channel-select.label = Radio Tuner Channel Name +channel-type.emotiva.tuner-channel-select.description = Name of user selected radio channel +channel-type.emotiva.tuner-channel-select.state.option.channel-1 = Channel 1 +channel-type.emotiva.tuner-channel-select.state.option.channel-2 = Channel 2 +channel-type.emotiva.tuner-channel-select.state.option.channel-3 = Channel 3 +channel-type.emotiva.tuner-channel-select.state.option.channel-4 = Channel 4 +channel-type.emotiva.tuner-channel-select.state.option.channel-5 = Channel 5 +channel-type.emotiva.tuner-channel-select.state.option.channel-6 = Channel 6 +channel-type.emotiva.tuner-channel-select.state.option.channel-7 = Channel 7 +channel-type.emotiva.tuner-channel-select.state.option.channel-8 = Channel 8 +channel-type.emotiva.tuner-channel-select.state.option.channel-9 = Channel 9 +channel-type.emotiva.tuner-channel-select.state.option.channel-10 = Channel 10 +channel-type.emotiva.tuner-channel-select.state.option.channel-11 = Channel 11 +channel-type.emotiva.tuner-channel-select.state.option.channel-12 = Channel 12 +channel-type.emotiva.tuner-channel-select.state.option.channel-13 = Channel 13 +channel-type.emotiva.tuner-channel-select.state.option.channel-14 = Channel 14 +channel-type.emotiva.tuner-channel-select.state.option.channel-15 = Channel 15 +channel-type.emotiva.tuner-channel-select.state.option.channel-16 = Channel 16 +channel-type.emotiva.tuner-channel-select.state.option.channel-17 = Channel 17 +channel-type.emotiva.tuner-channel-select.state.option.channel-18 = Channel 18 +channel-type.emotiva.tuner-channel-select.state.option.channel-19 = Channel 19 +channel-type.emotiva.tuner-channel-select.state.option.channel-20 = Channel 20 +channel-type.emotiva.tuner-program.label = Radio Tuner Program +channel-type.emotiva.tuner-program.description = Radio tuner program: "Country", "Rock", "Classical", etc. +channel-type.emotiva.tuner-program.state.option.adult-hits = Adult Hits +channel-type.emotiva.tuner-program.state.option.alarm = Alarm +channel-type.emotiva.tuner-program.state.option.alarm-test = Alarm Test +channel-type.emotiva.tuner-program.state.option.children-programmes = Children's Programmes +channel-type.emotiva.tuner-program.state.option.classic-rock = Classic Rock +channel-type.emotiva.tuner-program.state.option.classical = Classical +channel-type.emotiva.tuner-program.state.option.college = College +channel-type.emotiva.tuner-program.state.option.country-music = Country Music +channel-type.emotiva.tuner-program.state.option.culture = Culture +channel-type.emotiva.tuner-program.state.option.current-affairs = Current Affairs +channel-type.emotiva.tuner-program.state.option.documentary = Documentary +channel-type.emotiva.tuner-program.state.option.drama = Drama +channel-type.emotiva.tuner-program.state.option.easy-listening = Easy Listening +channel-type.emotiva.tuner-program.state.option.education = Education +channel-type.emotiva.tuner-program.state.option.emergency = Emergency +channel-type.emotiva.tuner-program.state.option.emergency-test = Emergency Test +channel-type.emotiva.tuner-program.state.option.finance = Finance +channel-type.emotiva.tuner-program.state.option.folk-music = Folk Music +channel-type.emotiva.tuner-program.state.option.information = Information +channel-type.emotiva.tuner-program.state.option.jazz = Jazz +channel-type.emotiva.tuner-program.state.option.jazz-music = Jazz Music +channel-type.emotiva.tuner-program.state.option.language = Language +channel-type.emotiva.tuner-program.state.option.leisure = Leisure +channel-type.emotiva.tuner-program.state.option.light-classical = Light Classical +channel-type.emotiva.tuner-program.state.option.national-music = National Music +channel-type.emotiva.tuner-program.state.option.news = News +channel-type.emotiva.tuner-program.state.option.nostalgia = Nostalgia +channel-type.emotiva.tuner-program.state.option.oldies = Oldies (Music) +channel-type.emotiva.tuner-program.state.option.oldies-music = Oldies Music +channel-type.emotiva.tuner-program.state.option.other-music = Other Music +channel-type.emotiva.tuner-program.state.option.personality = Personality +channel-type.emotiva.tuner-program.state.option.phone-in = Phone-in +channel-type.emotiva.tuner-program.state.option.popular-music = Popular Music (Pop) +channel-type.emotiva.tuner-program.state.option.public = Public +channel-type.emotiva.tuner-program.state.option.religion = Religion +channel-type.emotiva.tuner-program.state.option.religious-talk = Religious Talk +channel-type.emotiva.tuner-program.state.option.rhythm-blues = Rhythm & Blues +channel-type.emotiva.tuner-program.state.option.rock = Rock +channel-type.emotiva.tuner-program.state.option.rock-music = Rock Music +channel-type.emotiva.tuner-program.state.option.science = Science +channel-type.emotiva.tuner-program.state.option.serious-classical = Serious Classical +channel-type.emotiva.tuner-program.state.option.social-affairs = Social Affairs +channel-type.emotiva.tuner-program.state.option.soft-music = Soft Music +channel-type.emotiva.tuner-program.state.option.soft-rhythm-blues = Soft Rhythm & Blues +channel-type.emotiva.tuner-program.state.option.soft-rock = Soft Rock +channel-type.emotiva.tuner-program.state.option.sport = Sport +channel-type.emotiva.tuner-program.state.option.talk = Talk +channel-type.emotiva.tuner-program.state.option.top-40 = Top 40 +channel-type.emotiva.tuner-program.state.option.travel = Travel +channel-type.emotiva.tuner-program.state.option.weather = Weather +channel-type.emotiva.tuner-rds.label = Radio Tuner RDS +channel-type.emotiva.tuner-rds.description = Message from Radio Data System (RDS) for selected channel +channel-type.emotiva.tuner-signal.label = Radio Tuner Signal +channel-type.emotiva.tuner-signal.description = Radio tuner signal quality +channel-type.emotiva.video-format.label = Video Input Format +channel-type.emotiva.video-format.description = Current video input format: "1920x1080i/60", "3840x2160p/60", etc. +channel-type.emotiva.video-input.label = Video Input +channel-type.emotiva.video-input.description = Source for video input +channel-type.emotiva.video-space.label = Video Input Space +channel-type.emotiva.video-space.description = Current video input space: "YcbCr 8bits", etc. +channel-type.emotiva.volume.label = Volume +channel-type.emotiva.volume.description = Set the volume level of this zone +channel-type.emotiva.volume-db.label = Volume (dB) +channel-type.emotiva.volume-db.description = Set the volume level (dB). +channel-type.emotiva.volume-speaker-db.label = Speaker Trim +channel-type.emotiva.volume-speaker-db.description = Increased/Reduced volume for the speaker, treble or bass, in +/-dB +channel-type.emotiva.zonePower.label = Power (zone) +channel-type.emotiva.zonePower.description = Power ON/OFF this zone of the unit + + +# User Messages +message.processor.connecting = Connecting +message.processor.connection.failed = Failed to connect, check network connectivity and configuration +message.processor.connection.error.keep-alive = Failed to receive keepAlive message from device, check network connectivity! +message.processor.connection.error.port = portNumber is invalid! +message.processor.connection.error.address-empty = IP Address must be configured! +message.processor.connection.error.address-invalid = IP Address is not valid! +message.processor.notfound = Could not find device with ipAddress {0} +message.processor.goodbye = Device was shutdown diff --git a/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..83101e3f77e --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,532 @@ + + + + + + Emotiva Processor Thing for Emotiva Binding + + + + + + Channels for the main zone of this processor + + + + Channels for zone2 of this processor + + + + + Unknown Model + Unknown Model Revision + Unknown Data Revision + + + ipAddress + + + + + + + General channels for this processor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Channels for a zone of this processor + + + + + + + + + + + Switch + + Power ON/OFF the device + + + + Switch + + Power ON/OFF this zone of the Processor + + + + Dimmer + + Set the volume level of this zone + SoundVolume + + + + + Number:Dimensionless + + Set the volume level (dB). Same as [mainVolume - 96] + SoundVolume + + + + + Switch + + Enable or Disable Mute on this zone of the Processor + SoundVolume + + + + String + + Select the input source for this zone of the Processor + recommend + + + + Switch + + Set appliance on standby + Energy + + + + String + + Menu display ON/OFF for the device + + + + String + + Menu Control for emulating an Emotiva Remote control + + + + String + + Menu Control Up + + + + String + + Menu Control Down + + + + String + + Menu Control Left + + + + String + + Menu Control Right + + + + String + + Menu Control Enter + + + + Number + + Increased/Reduced volume for a given speaker, in dB + SoundVolume + + + + + Number:Dimensionless + + Percentage of dimness: "0", "20", "40", "60", "80", "100" + Light + + + + + String + + Main zone mode: "Stereo", "Direct", "Auto", etc. + + + + + + + + + + + + + + + + String + + Shown Info Screen + + + + String + + Speaker preset Name + + + + + + + + + + Switch + + Enable/Disable Loudness on this zone of the Processor + + + + Rollershutter + + Radio Tuner frequency + + + + Rollershutter + + Radio Tuner seek + + + + Rollershutter + + Radio Tuner Channel + + + + + String + + Radio tuner band: "AM" or "FM" + + + + + + + + + + Number:Frequency + + User select radio tuner channel frequency" + + + + + String + + User select radio tuner channel name + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + Radio tuner signal quality + + + + + String + + Radio tuner program: "Country", "Rock", "Classical", etc. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + Radio Data System (RDS) tuner string + + + + + String + + Input source for audio on main zone + + + + + String + + Audio input bitstream type: "PCM 2.0", "ATMOS", etc. + + + + + String + + Audio input bits: "32kHZ 24bits", etc. + + + + + String + + Input source for video on main zone + + + + + String + + Video input format: "1920x1080i/60", "3840x2160p/60", etc. + + + + + String + + Video input space: "YcbCr 8bits", etc. + + + + + String + + Custom Input Name + + + + + + String + + User selected mode for the main zone. An "Auto" value here might not mean the mode channel is in + auto: + "Stereo", "Direct", "Auto", etc. + + + + + + + + + + + + + + + + + String + + User-selected movie or music mode for main zone: "Movie" or "Music" + + + + + + + + + + String + + Main zone surround mode: "Auto", "Stereo", "Dolby", ... + + + + + + + + + + + + + + + + String + + Text displayed on front panel bar of device + + + + + String + + Text displayed on a specific menu row and column + + + diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/AbstractDTOTestBase.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/AbstractDTOTestBase.java new file mode 100644 index 00000000000..d38faae064a --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/AbstractDTOTestBase.java @@ -0,0 +1,311 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils; + +/** + * Abstract helper class for unit tests. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class AbstractDTOTestBase { + + protected EmotivaXmlUtils xmlUtils = new EmotivaXmlUtils(); + + protected String emotivaAckPowerOff = """ + + + + """; + + protected String emotivaAckPowerOffAndNotRealCommand = """ + + + + + """; + + protected String emotivaAckPowerOffAndVolume = """ + + + + + """; + + protected String emotivaCommandoPowerOn = """ + """; + + protected String emotivaNotifyEmotivaPropertyPower = """ + """; + + protected String emotivaUpdateEmotivaPropertyPower = """ + """; + + protected String emotivaControlVolume = """ + + + """; + + protected String emotivaNotifyV2KeepAlive = """ + + + + """; + + protected String emotivaNotifyV2UnknownTag = """ + + + + """; + + protected String emotivaNotifyV2KeepAliveSequence = "54062"; + + protected String emotivaNotifyV3KeepAlive = """ + + + + """; + + protected String emotivaNotifyV3EmptyMenuValue = """ + + + + + """; + + protected String emotivaUpdateRequest = """ + + + + + + + + + + + """; + + protected String emotivaMenuNotify = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; + + protected String emotivaMenuNotifyWithCheckBox = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; + + protected String emotivaMenuNotifyProgress = """ + + + + """; + + protected String emotivaUpdateResponseV2 = """ + + + + + + """; + + protected String emotivaUpdateResponseV3 = """ + + + + + + """; + + protected String emotivaBarNotifyBigText = """ + + + + """; + + protected String emotivaSubscriptionRequest = """ + + + + + """; + + protected String emotivaSubscriptionResponse = """ + + + + + + + + """; + + protected String emotivaPingV2 = """ + + """; + + protected String emotivaPingV3 = """ + + """; + + protected String emotivaTransponderResponseV2 = """ + + + XMC-1 + 2.0 + Living Room + + 2.0 + 7002 + 7003 + 7004 + 7100 + 10000 + + """; + + protected String emotivaTransponderResponseV3 = """ + + + XMC-2 + 3.0 + Living Room + + 3.0 + 7002 + 7003 + 7004 + 7100 + 10000 + + """; + + public AbstractDTOTestBase() throws JAXBException { + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelperTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelperTest.java new file mode 100644 index 00000000000..b495509cb16 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelperTest.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MAIN_VOLUME; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MUTE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_STANDBY; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SURROUND; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumeDecibelToPercentage; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumePercentageToDecibel; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.mute; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.mute_off; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.mute_on; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.standby; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.surround; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.surround_trim_set; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.volume; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.DIMENSIONLESS_DECIBEL; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.ON_OFF; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V2; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V3; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlRequest; +import org.openhab.binding.emotiva.internal.protocol.EmotivaDataType; +import org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion; +import org.openhab.core.library.types.PercentType; + +/** + * Unit tests for the EmotivaCommandHelper. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaCommandHelperTest { + + @Test + void volumeToPercentage() { + assertThat(volumeDecibelToPercentage("-100 dB"), is(PercentType.valueOf("0"))); + assertThat(volumeDecibelToPercentage(" -96"), is(PercentType.valueOf("0"))); + assertThat(volumeDecibelToPercentage("-41 dB "), is(PercentType.valueOf("50"))); + assertThat(volumeDecibelToPercentage("15"), is(PercentType.valueOf("100"))); + assertThat(volumeDecibelToPercentage("20"), is(PercentType.valueOf("100"))); + } + + @Test + void volumeToDecibel() { + assertThat(volumePercentageToDecibel("-10"), is(-96)); + assertThat(volumePercentageToDecibel("0%"), is(-96)); + assertThat(volumePercentageToDecibel("50 %"), is(-41)); + assertThat(volumePercentageToDecibel("100 % "), is(15)); + assertThat(volumePercentageToDecibel("110"), is(15)); + } + + private static Stream channelToControlRequest() { + return Stream.of( + Arguments.of(CHANNEL_SURROUND, "surround", DIMENSIONLESS_DECIBEL, surround, surround, surround, + surround_trim_set, PROTOCOL_V2, -24.0, 24.0), + Arguments.of(CHANNEL_SURROUND, "surround", DIMENSIONLESS_DECIBEL, surround, surround, surround, + surround_trim_set, PROTOCOL_V3, -24.0, 24.0), + Arguments.of(CHANNEL_MUTE, "mute", ON_OFF, mute, mute_on, mute_off, mute, PROTOCOL_V2, 0, 0), + Arguments.of(CHANNEL_STANDBY, "standby", ON_OFF, standby, standby, standby, standby, PROTOCOL_V2, 0, 0), + Arguments.of(CHANNEL_MAIN_VOLUME, "volume", DIMENSIONLESS_DECIBEL, volume, volume, volume, volume, + PROTOCOL_V2, -96, 15)); + } + + @ParameterizedTest + @MethodSource("channelToControlRequest") + void testChannelToControlRequest(String channel, String name, EmotivaDataType emotivaDataType, + EmotivaControlCommands defaultCommand, EmotivaControlCommands onCommand, EmotivaControlCommands offCommand, + EmotivaControlCommands setCommand, EmotivaProtocolVersion version, double min, double max) { + final Map> commandMaps = new ConcurrentHashMap<>(); + + EmotivaControlRequest surround = EmotivaCommandHelper.channelToControlRequest(channel, commandMaps, version); + assertThat(surround.getName(), is(name)); + assertThat(surround.getChannel(), is(channel)); + assertThat(surround.getDataType(), is(emotivaDataType)); + assertThat(surround.getDefaultCommand(), is(defaultCommand)); + assertThat(surround.getOnCommand(), is(onCommand)); + assertThat(surround.getOffCommand(), is(offCommand)); + assertThat(surround.getSetCommand(), is(setCommand)); + assertThat(surround.getMinValue(), is(min)); + assertThat(surround.getMaxValue(), is(max)); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTOTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTOTest.java new file mode 100644 index 00000000000..1fc2c89e6f5 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTOTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; + +/** + * Unit tests for EmotivaAck message type. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaAckDTOTest extends AbstractDTOTestBase { + + public EmotivaAckDTOTest() throws JAXBException { + } + + @Test + void unmarshallValidCommand() throws JAXBException { + EmotivaAckDTO dto = (EmotivaAckDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaAckPowerOff); + assertThat(dto, is(notNullValue())); + assertThat(dto.getCommands().size(), is(1)); + } + + @Test + void unmarshallOneValidCommand() throws JAXBException { + EmotivaAckDTO dto = (EmotivaAckDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaAckPowerOffAndNotRealCommand); + assertThat(dto, is(notNullValue())); + List commands = xmlUtils.unmarshallXmlObjectsToControlCommands(dto.getCommands()); + assertThat(commands.size(), is(2)); + + assertThat(commands.get(0), is(notNullValue())); + assertThat(commands.get(0).getName(), is(EmotivaControlCommands.power_off.name())); + assertThat(commands.get(0).getStatus(), is("ack")); + assertThat(commands.get(0).getVisible(), is(nullValue())); + assertThat(commands.get(0).getValue(), is(nullValue())); + + assertThat(commands.get(1), is(notNullValue())); + assertThat(commands.get(1).getName(), is(EmotivaControlCommands.none.name())); + assertThat(commands.get(1).getStatus(), is("ack")); + assertThat(commands.get(1).getVisible(), is(nullValue())); + assertThat(commands.get(1).getValue(), is(nullValue())); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTOTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTOTest.java new file mode 100644 index 00000000000..da618750bbc --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTOTest.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; + +/** + * Unit tests for EmotivaBarNotify message type. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaBarNotifyDTOTest extends AbstractDTOTestBase { + + public EmotivaBarNotifyDTOTest() throws JAXBException { + } + + @Test + void testUnmarshall() throws JAXBException { + EmotivaBarNotifyWrapper dto = (EmotivaBarNotifyWrapper) xmlUtils + .unmarshallToEmotivaDTO(emotivaBarNotifyBigText); + assertThat(dto.getSequence(), is("98")); + assertThat(dto.getTags().size(), is(1)); + + List commands = xmlUtils.unmarshallToBarNotify(dto.getTags()); + assertThat(commands.get(0).getType(), is("bigText")); + assertThat(commands.get(0).getText(), is("XBox One")); + assertThat(commands.get(0).getUnits(), is(nullValue())); + assertThat(commands.get(0).getMin(), is(nullValue())); + assertThat(commands.get(0).getMax(), is(nullValue())); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTOTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTOTest.java new file mode 100644 index 00000000000..bf7f5f6e5d5 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTOTest.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; + +/** + * Unit tests for EmotivaCommandDTO command types. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaCommandDTOTest extends AbstractDTOTestBase { + + public EmotivaCommandDTOTest() throws JAXBException { + } + + @Test + void unmarshallElements() { + List commandDTO = xmlUtils.unmarshallToCommands(emotivaCommandoPowerOn); + assertThat(commandDTO, is(notNullValue())); + assertThat(commandDTO.size(), is(1)); + assertThat(commandDTO.get(0).getName(), is(EmotivaControlCommands.power_on.name())); + } + + @Test + void unmarshallFromEmotivaAckWithMissingEnumType() { + List commandDTO = xmlUtils.unmarshallToCommands(emotivaAckPowerOffAndNotRealCommand); + assertThat(commandDTO, is(notNullValue())); + assertThat(commandDTO.size(), is(2)); + assertThat(commandDTO.get(0).getName(), is(EmotivaControlCommands.power_off.name())); + assertThat(commandDTO.get(0).getStatus(), is("ack")); + assertThat(commandDTO.get(0).getValue(), is(nullValue())); + assertThat(commandDTO.get(0).getVisible(), is(nullValue())); + assertThat(commandDTO.get(1).getName(), is(EmotivaControlCommands.none.name())); + assertThat(commandDTO.get(1).getStatus(), is("ack")); + assertThat(commandDTO.get(1).getValue(), is(nullValue())); + assertThat(commandDTO.get(1).getVisible(), is(nullValue())); + } + + @Test + void unmarshallFromEmotivaAck() { + List commandDTO = xmlUtils.unmarshallToCommands(emotivaAckPowerOffAndVolume); + assertThat(commandDTO, is(notNullValue())); + assertThat(commandDTO.size(), is(2)); + assertThat(commandDTO.get(0).getName(), is(EmotivaControlCommands.power_off.name())); + assertThat(commandDTO.get(0).getStatus(), is("ack")); + assertThat(commandDTO.get(0).getValue(), is(nullValue())); + assertThat(commandDTO.get(0).getVisible(), is(nullValue())); + assertThat(commandDTO.get(1).getName(), is(EmotivaControlCommands.volume.name())); + assertThat(commandDTO.get(1).getStatus(), is("ack")); + assertThat(commandDTO.get(1).getValue(), is(nullValue())); + assertThat(commandDTO.get(1).getVisible(), is(nullValue())); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTOTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTOTest.java new file mode 100644 index 00000000000..a27e2ab0673 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTOTest.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Collections; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; + +/** + * Unit tests for EmotivaControl message type. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaControlDTOTest extends AbstractDTOTestBase { + + public EmotivaControlDTOTest() throws JAXBException { + } + + @Test + void marshalWithNoCommand() { + EmotivaControlDTO control = new EmotivaControlDTO(null); + String xmlString = xmlUtils.marshallJAXBElementObjects(control); + assertThat(xmlString, containsString("")); + assertThat(xmlString, not(containsString(""))); + } + + @Test + void marshalNoCommand() { + EmotivaControlDTO control = new EmotivaControlDTO(Collections.emptyList()); + String xmlString = xmlUtils.marshallJAXBElementObjects(control); + assertThat(xmlString, containsString("")); + } + + @Test + void marshalCommand() { + EmotivaCommandDTO command = EmotivaCommandDTO.fromTypeWithAck(EmotivaControlCommands.set_volume, "10"); + EmotivaControlDTO control = new EmotivaControlDTO(List.of(command)); + String xmlString = xmlUtils.marshallJAXBElementObjects(control); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, endsWith("\n")); + } + + @Test + void marshalWithTwoCommands() { + EmotivaControlDTO control = new EmotivaControlDTO( + List.of(EmotivaCommandDTO.fromTypeWithAck(EmotivaControlCommands.power_on), + EmotivaCommandDTO.fromTypeWithAck(EmotivaControlCommands.hdmi1))); + String xmlString = xmlUtils.marshallJAXBElementObjects(control); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, endsWith("\n")); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTOTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTOTest.java new file mode 100644 index 00000000000..254a2dd339f --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTOTest.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; + +/** + * Unit tests for EmotivaMenuNotify message type. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaMenuNotifyDTOTest extends AbstractDTOTestBase { + + public EmotivaMenuNotifyDTOTest() throws JAXBException { + } + + @Test + void testUnmarshallMenu() throws JAXBException { + EmotivaMenuNotifyDTO dto = (EmotivaMenuNotifyDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaMenuNotify); + assertThat(dto.getProgress(), is(nullValue())); + assertThat(dto.getSequence(), is("2378")); + assertThat(dto.getRow().size(), is(11)); + assertThat(dto.getRow().size(), is(11)); + assertThat(dto.getRow().get(0).getNumber(), is("0")); + assertThat(dto.getRow().get(0).getCol().size(), is(3)); + assertThat(dto.getRow().get(0).getCol().get(0).getNumber(), is("0")); + assertThat(dto.getRow().get(0).getCol().get(0).getValue(), is("")); + assertThat(dto.getRow().get(0).getCol().get(0).getHighlight(), is("no")); + assertThat(dto.getRow().get(0).getCol().get(0).getArrow(), is("no")); + assertThat(dto.getRow().get(0).getCol().get(1).getNumber(), is("1")); + assertThat(dto.getRow().get(0).getCol().get(1).getValue(), is("Left Display")); + assertThat(dto.getRow().get(0).getCol().get(1).getHighlight(), is("no")); + assertThat(dto.getRow().get(0).getCol().get(1).getArrow(), is("up")); + assertThat(dto.getRow().get(0).getCol().get(2).getNumber(), is("2")); + assertThat(dto.getRow().get(0).getCol().get(2).getValue(), is("Full Status")); + assertThat(dto.getRow().get(0).getCol().get(2).getHighlight(), is("no")); + assertThat(dto.getRow().get(0).getCol().get(2).getArrow(), is("no")); + } + + @Test + void testUnmarshallProgress() throws JAXBException { + EmotivaMenuNotifyDTO dto = (EmotivaMenuNotifyDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaMenuNotifyProgress); + assertThat(dto.getSequence(), is("2405")); + assertThat(dto.getRow(), is(nullValue())); + assertThat(dto.getProgress().getTime(), is("15")); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapperTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapperTest.java new file mode 100644 index 00000000000..bdee59e1c8d --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapperTest.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Collections; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; +import org.w3c.dom.Element; + +/** + * Unit tests for EmotivaNotify wrapper. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaNotifyWrapperTest extends AbstractDTOTestBase { + + public EmotivaNotifyWrapperTest() throws JAXBException { + } + + @Test + void marshallWithNoProperty() { + EmotivaNotifyWrapper dto = new EmotivaNotifyWrapper(emotivaNotifyV2KeepAliveSequence, Collections.emptyList()); + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString, + containsString("")); + assertThat(xmlAsString, not(containsString(""))); + } + + @Test + void marshallWithOneProperty() { + List keepAliveProperty = List.of(new EmotivaPropertyDTO("keepAlive", "7500", "true")); + EmotivaNotifyWrapper dto = new EmotivaNotifyWrapper(emotivaNotifyV2KeepAliveSequence, keepAliveProperty); + + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString, + containsString("")); + assertThat(xmlAsString, containsString("")); + assertThat(xmlAsString, containsString("")); + } + + @Test + void testUnmarshallV2() throws JAXBException { + EmotivaNotifyWrapper dto = (EmotivaNotifyWrapper) xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2KeepAlive); + assertThat(dto.getSequence(), is(emotivaNotifyV2KeepAliveSequence)); + assertThat(dto.getTags().size(), is(1)); + assertThat(dto.getTags().get(0), instanceOf(Element.class)); + Element keepAlive = (Element) dto.getTags().get(0); + assertThat(keepAlive.getTagName(), is(EmotivaSubscriptionTags.keepAlive.name())); + assertThat(keepAlive.hasAttribute("value"), is(true)); + assertThat(keepAlive.getAttribute("value"), is("7500")); + assertThat(keepAlive.hasAttribute("visible"), is(true)); + assertThat(keepAlive.getAttribute("visible"), is("true")); + assertThat(dto.getProperties(), is(nullValue())); + } + + @Test + void testUnmarshallV2UnknownProperty() throws JAXBException { + EmotivaNotifyWrapper dto = (EmotivaNotifyWrapper) xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2UnknownTag); + assertThat(dto.getSequence(), is(emotivaNotifyV2KeepAliveSequence)); + assertThat(dto.getTags().size(), is(1)); + assertThat(dto.getTags().get(0), instanceOf(Element.class)); + Element unknownCommand = (Element) dto.getTags().get(0); + assertThat(unknownCommand.getTagName(), is("unknownTag")); + assertThat(dto.getProperties(), is(nullValue())); + } + + @Test + void testUnmarshallV3() throws JAXBException { + EmotivaNotifyWrapper dto = (EmotivaNotifyWrapper) xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV3KeepAlive); + assertThat(dto.getSequence(), is(emotivaNotifyV2KeepAliveSequence)); + assertThat(dto.getProperties().size(), is(1)); + assertThat(dto.getTags(), is(nullValue())); + } + + @Test + void testUnmarshallV3EmptyValue() throws JAXBException { + EmotivaNotifyWrapper dto = (EmotivaNotifyWrapper) xmlUtils + .unmarshallToEmotivaDTO(emotivaNotifyV3EmptyMenuValue); + assertThat(dto.getSequence(), is("23929")); + assertThat(dto.getProperties().size(), is(1)); + assertThat(dto.getProperties().get(0).getName(), is("menu")); + assertThat(dto.getProperties().get(0).getValue(), is("")); + assertThat(dto.getProperties().get(0).getVisible(), is("true")); + assertThat(dto.getProperties().get(0).getStatus(), is(notNullValue())); + assertThat(dto.getTags(), is(nullValue())); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTOTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTOTest.java new file mode 100644 index 00000000000..8a7f7ee3362 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTOTest.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; + +/** + * Unit tests for EmotivaPing message type. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaPingDTOTest extends AbstractDTOTestBase { + + public EmotivaPingDTOTest() throws JAXBException { + } + + @Test + void marshallPlain() { + EmotivaPingDTO dto = new EmotivaPingDTO(); + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString, containsString("")); + assertThat(xmlAsString, not(containsString(""))); + } + + @Test + void marshallWithProtocol() { + EmotivaPingDTO dto = new EmotivaPingDTO("3.0"); + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString, containsString("")); + assertThat(xmlAsString, not(containsString(""))); + } + + @Test + void unmarshallV2() throws JAXBException { + EmotivaPingDTO dto = (EmotivaPingDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaPingV2); + assertThat(dto, is(notNullValue())); + assertThat(dto.getProtocol(), is(nullValue())); + } + + @Test + void unmarshallV3() throws JAXBException { + EmotivaPingDTO dto = (EmotivaPingDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaPingV3); + assertThat(dto, is(notNullValue())); + assertThat(dto.getProtocol(), is(notNullValue())); + assertThat(dto.getProtocol(), is("3.0")); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTOTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTOTest.java new file mode 100644 index 00000000000..d9cc821ba43 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTOTest.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus.VALID; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * Unit tests for EmotivaCommandDTO command types. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaPropertyDTOTest extends AbstractDTOTestBase { + + public EmotivaPropertyDTOTest() throws JAXBException { + } + + @Test + void unmarshallFromEmotivaNotify() throws JAXBException { + EmotivaPropertyDTO commandDTO = (EmotivaPropertyDTO) xmlUtils + .unmarshallToEmotivaDTO(emotivaNotifyEmotivaPropertyPower); + assertThat(commandDTO, is(notNullValue())); + assertThat(commandDTO.getName(), is(EmotivaSubscriptionTags.tuner_channel.name())); + assertThat(commandDTO.getValue(), is("FM 106.50MHz")); + assertThat(commandDTO.getVisible(), is("true")); + assertThat(commandDTO.getStatus(), is(notNullValue())); + } + + @Test + void unmarshallFromEmotivaUpdate() throws JAXBException { + EmotivaPropertyDTO commandDTO = (EmotivaPropertyDTO) xmlUtils + .unmarshallToEmotivaDTO(emotivaUpdateEmotivaPropertyPower); + assertThat(commandDTO, is(notNullValue())); + assertThat(commandDTO.getName(), is(EmotivaControlCommands.power.name())); + assertThat(commandDTO.getValue(), is("On")); + assertThat(commandDTO.getVisible(), is("true")); + assertThat(commandDTO.getStatus(), is(VALID.getValue())); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequestTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequestTest.java new file mode 100644 index 00000000000..8f3c9a8b781 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequestTest.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_RDS; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V2; + +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * Unit tests for EmotivaSubscription requests. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaSubscriptionRequestTest extends AbstractDTOTestBase { + + public EmotivaSubscriptionRequestTest() throws JAXBException { + } + + @Test + void marshalFromChannelUID() { + EmotivaSubscriptionTags subscriptionChannel = EmotivaSubscriptionTags.fromChannelUID(CHANNEL_TUNER_RDS); + EmotivaSubscriptionRequest emotivaSubscriptionRequest = new EmotivaSubscriptionRequest(subscriptionChannel); + String xmlString = xmlUtils.marshallJAXBElementObjects(emotivaSubscriptionRequest); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + } + + @Test + void marshallWithTwoSubscriptionsNoAck() { + EmotivaCommandDTO command1 = new EmotivaCommandDTO(EmotivaControlCommands.volume, "10", "yes"); + EmotivaCommandDTO command2 = new EmotivaCommandDTO(EmotivaControlCommands.power_off); + + EmotivaSubscriptionRequest dto = new EmotivaSubscriptionRequest(List.of(command1, command2), + PROTOCOL_V2.value()); + + String xmlString = xmlUtils.marshallJAXBElementObjects(dto); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, not(containsString(""))); + assertThat(xmlString, not(containsString(""))); + } + + @Test + void unmarshall() throws JAXBException { + var dto = (EmotivaSubscriptionResponse) xmlUtils.unmarshallToEmotivaDTO(emotivaSubscriptionRequest); + assertThat(dto, is(notNullValue())); + assertThat(dto.getTags().size(), is(3)); + assertThat(dto.getProperties(), is(nullValue())); + + List commands = xmlUtils.unmarshallToNotification(dto.getTags()); + + assertThat(commands.get(0).getName(), is(EmotivaSubscriptionTags.selected_mode.name())); + assertThat(commands.get(0).getStatus(), is(nullValue())); + assertThat(commands.get(0).getValue(), is(nullValue())); + assertThat(commands.get(0).getVisible(), is(nullValue())); + + assertThat(commands.get(1).getName(), is(EmotivaSubscriptionTags.power.name())); + assertThat(commands.get(1).getStatus(), is(nullValue())); + assertThat(commands.get(1).getValue(), is(nullValue())); + assertThat(commands.get(1).getVisible(), is(nullValue())); + + assertThat(commands.get(2).getName(), is("unknown")); + assertThat(commands.get(2).getStatus(), is(nullValue())); + assertThat(commands.get(2).getValue(), is(nullValue())); + assertThat(commands.get(2).getVisible(), is(nullValue())); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponseTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponseTest.java new file mode 100644 index 00000000000..e0dda275194 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponseTest.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.power_on; + +import java.util.Collections; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * Unit tests for EmotivaSubscription responses. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaSubscriptionResponseTest extends AbstractDTOTestBase { + + public EmotivaSubscriptionResponseTest() throws JAXBException { + } + + @Test + void marshallNoProperty() { + var dto = new EmotivaSubscriptionResponse(Collections.emptyList()); + String xmlString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlString, containsString("")); + assertThat(xmlString, not(containsString(""))); + assertThat(xmlString, not(containsString(""))); + assertThat(xmlString, not(containsString(""))); + } + + @Test + void marshallWithOneProperty() { + EmotivaPropertyDTO emotivaPropertyDTO = new EmotivaPropertyDTO(power_on.name(), "On", "true"); + var dto = new EmotivaSubscriptionResponse(Collections.singletonList(emotivaPropertyDTO)); + String xmlString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, not(containsString(""))); + assertThat(xmlString, not(containsString(""))); + assertThat(xmlString, containsString("")); + } + + @Test + void unmarshall() throws JAXBException { + var dto = (EmotivaSubscriptionResponse) xmlUtils.unmarshallToEmotivaDTO(emotivaSubscriptionResponse); + assertThat(dto.tags, is(notNullValue())); + assertThat(dto.tags.size(), is(5)); + List commands = xmlUtils.unmarshallToNotification(dto.getTags()); + assertThat(commands, is(notNullValue())); + assertThat(commands.size(), is(dto.tags.size())); + assertThat(commands.get(0), instanceOf(EmotivaNotifyDTO.class)); + assertThat(commands.get(0).getName(), is(EmotivaSubscriptionTags.power.name())); + assertThat(commands.get(0).getStatus(), is(EmotivaPropertyStatus.VALID.getValue())); + assertThat(commands.get(0).getVisible(), is(nullValue())); + assertThat(commands.get(0).getValue(), is(nullValue())); + + assertThat(commands.get(1).getName(), is(EmotivaSubscriptionTags.source.name())); + assertThat(commands.get(1).getValue(), is("SHIELD ")); + assertThat(commands.get(1).getVisible(), is("true")); + assertThat(commands.get(1).getStatus(), is(EmotivaPropertyStatus.VALID.getValue())); + + assertThat(commands.get(2).getName(), is(EmotivaSubscriptionTags.menu.name())); + assertThat(commands.get(2).getValue(), is("Off")); + assertThat(commands.get(2).getVisible(), is("true")); + assertThat(commands.get(2).getStatus(), is(EmotivaPropertyStatus.VALID.getValue())); + + assertThat(commands.get(3).getName(), is(EmotivaSubscriptionTags.treble.name())); + assertThat(commands.get(3).getValue(), is("+ 1.5")); + assertThat(commands.get(3).getVisible(), is("true")); + assertThat(commands.get(3).getStatus(), is(EmotivaPropertyStatus.VALID.getValue())); + assertThat(commands.get(3).getAck(), is("yes")); + + assertThat(commands.get(4).getName(), is(EmotivaSubscriptionTags.UNKNOWN_TAG)); + assertThat(commands.get(4).getValue(), is(nullValue())); + assertThat(commands.get(4).getVisible(), is(nullValue())); + assertThat(commands.get(4).getStatus(), is(nullValue())); + assertThat(commands.get(4).getAck(), is("no")); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTOTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTOTest.java new file mode 100644 index 00000000000..f97232b8030 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTOTest.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; + +/** + * Unit tests for EmotivaTransponder message type. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaTransponderDTOTest extends AbstractDTOTestBase { + + public EmotivaTransponderDTOTest() throws JAXBException { + } + + @Test + void unmarshallV2() throws JAXBException { + EmotivaTransponderDTO dto = (EmotivaTransponderDTO) xmlUtils + .unmarshallToEmotivaDTO(emotivaTransponderResponseV2); + assertThat(dto, is(notNullValue())); + assertThat(dto.getModel(), is("XMC-1")); + assertThat(dto.getRevision(), is("2.0")); + assertThat(dto.getName(), is("Living Room")); + assertThat(dto.getControl().getVersion(), is("2.0")); + assertThat(dto.getControl().getControlPort(), is(7002)); + assertThat(dto.getControl().getNotifyPort(), is(7003)); + assertThat(dto.getControl().getInfoPort(), is(7004)); + assertThat(dto.getControl().getSetupPortTCP(), is(7100)); + assertThat(dto.getControl().getKeepAlive(), is(10000)); + } + + @Test + void unmarshallV3() throws JAXBException { + EmotivaTransponderDTO dto = (EmotivaTransponderDTO) xmlUtils + .unmarshallToEmotivaDTO(emotivaTransponderResponseV3); + assertThat(dto, is(notNullValue())); + assertThat(dto.getModel(), is("XMC-2")); + assertThat(dto.getRevision(), is("3.0")); + assertThat(dto.getName(), is("Living Room")); + assertThat(dto.getControl().getVersion(), is("3.0")); + assertThat(dto.getControl().getControlPort(), is(7002)); + assertThat(dto.getControl().getNotifyPort(), is(7003)); + assertThat(dto.getControl().getInfoPort(), is(7004)); + assertThat(dto.getControl().getSetupPortTCP(), is(7100)); + assertThat(dto.getControl().getKeepAlive(), is(10000)); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscriptionTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscriptionTest.java new file mode 100644 index 00000000000..95f4101e65e --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscriptionTest.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_RDS; + +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * Unit tests for EmotivaUnsubscribe requests. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaUnsubscriptionTest extends AbstractDTOTestBase { + + public EmotivaUnsubscriptionTest() throws JAXBException { + } + + @Test + void marshalFromChannelUID() { + EmotivaSubscriptionTags subscriptionChannel = EmotivaSubscriptionTags.fromChannelUID(CHANNEL_TUNER_RDS); + EmotivaUnsubscribeDTO emotivaSubscriptionRequest = new EmotivaUnsubscribeDTO(subscriptionChannel); + String xmlString = xmlUtils.marshallJAXBElementObjects(emotivaSubscriptionRequest); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + } + + @Test + void marshallWithTwoUnsubscriptions() { + EmotivaCommandDTO command1 = new EmotivaCommandDTO(EmotivaControlCommands.volume); + EmotivaCommandDTO command2 = new EmotivaCommandDTO(EmotivaControlCommands.power_off); + + EmotivaUnsubscribeDTO dto = new EmotivaUnsubscribeDTO(List.of(command1, command2)); + + String xmlString = xmlUtils.marshallJAXBElementObjects(dto); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, not(containsString(""))); + assertThat(xmlString, not(containsString(""))); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequestTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequestTest.java new file mode 100644 index 00000000000..9396fe74ae5 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequestTest.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Collections; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.EmotivaBindingConstants; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * Unit tests for EmotivaUpdate requests. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaUpdateRequestTest extends AbstractDTOTestBase { + + public EmotivaUpdateRequestTest() throws JAXBException { + } + + @Test + void marshallWithNoProperty() { + EmotivaUpdateRequest dto = new EmotivaUpdateRequest(Collections.emptyList()); + String xmlAsString = xmlUtils.marshallJAXBElementObjects(dto); + assertThat(xmlAsString, containsString("")); + assertThat(xmlAsString, not(containsString(""))); + } + + @Test + void marshalFromChannelUID() { + EmotivaSubscriptionTags subscriptionChannel = EmotivaSubscriptionTags + .fromChannelUID(EmotivaBindingConstants.CHANNEL_TUNER_RDS); + EmotivaUpdateRequest emotivaUpdateRequest = new EmotivaUpdateRequest(subscriptionChannel); + String xmlString = xmlUtils.marshallJAXBElementObjects(emotivaUpdateRequest); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + assertThat(xmlString, containsString("")); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponseTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponseTest.java new file mode 100644 index 00000000000..6c9f8788811 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponseTest.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.dto; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus.NOT_VALID; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus.VALID; + +import java.util.Collections; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * Unit tests for EmotivaUpdate responses. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaUpdateResponseTest extends AbstractDTOTestBase { + + public EmotivaUpdateResponseTest() throws JAXBException { + } + + @Test + void marshallWithNoProperty() { + EmotivaUpdateResponse dto = new EmotivaUpdateResponse(Collections.emptyList()); + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString, containsString("")); + assertThat(xmlAsString, not(containsString(""))); + } + + @Test + void unmarshallV2() throws JAXBException { + var dto = (EmotivaUpdateResponse) xmlUtils.unmarshallToEmotivaDTO(emotivaUpdateResponseV2); + assertThat(dto, is(notNullValue())); + assertThat(dto.getProperties(), is(nullValue())); + List notifications = xmlUtils.unmarshallToNotification(dto.getTags()); + assertThat(notifications.size(), is(3)); + + assertThat(notifications.get(0).getName(), is(EmotivaSubscriptionTags.power.name())); + assertThat(notifications.get(0).getValue(), is("On")); + assertThat(notifications.get(0).getVisible(), is("true")); + assertThat(notifications.get(0).getStatus(), is(VALID.getValue())); + + assertThat(notifications.get(1).getName(), is(EmotivaSubscriptionTags.source.name())); + assertThat(notifications.get(1).getValue(), is("HDMI 1")); + assertThat(notifications.get(1).getVisible(), is("true")); + assertThat(notifications.get(1).getStatus(), is(NOT_VALID.getValue())); + + assertThat(notifications.get(2).getName(), is(EmotivaSubscriptionTags.unknown.name())); + assertThat(notifications.get(2).getStatus(), is(nullValue())); + assertThat(notifications.get(2).getValue(), is(nullValue())); + assertThat(notifications.get(2).getVisible(), is(nullValue())); + } + + @Test + void unmarshallV3() throws JAXBException { + var dto = (EmotivaUpdateResponse) xmlUtils.unmarshallToEmotivaDTO(emotivaUpdateResponseV3); + assertThat(dto, is(notNullValue())); + assertThat(dto.getTags(), is(nullValue())); + assertThat(dto.getProperties().size(), is(3)); + + assertThat(dto.getProperties().get(0), instanceOf(EmotivaPropertyDTO.class)); + assertThat(dto.getProperties().get(0).getName(), is(EmotivaSubscriptionTags.power.name())); + assertThat(dto.getProperties().get(0).getValue(), is("On")); + assertThat(dto.getProperties().get(0).getVisible(), is("true")); + assertThat(dto.getProperties().get(0).getStatus(), is(VALID.getValue())); + + assertThat(dto.getProperties().get(1).getName(), is(EmotivaSubscriptionTags.source.name())); + assertThat(dto.getProperties().get(1).getValue(), is("HDMI 1")); + assertThat(dto.getProperties().get(1).getVisible(), is("true")); + assertThat(dto.getProperties().get(1).getStatus(), is(NOT_VALID.getValue())); + + assertThat(dto.getProperties().get(2).getName(), is("noKnownTag")); + assertThat(dto.getProperties().get(2).getStatus(), is(notNullValue())); + assertThat(dto.getProperties().get(2).getValue(), is(notNullValue())); + assertThat(dto.getProperties().get(2).getVisible(), is(notNullValue())); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequestTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequestTest.java new file mode 100644 index 00000000000..754f18a1cd6 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequestTest.java @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.*; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.*; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_band; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_channel; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.binding.emotiva.internal.EmotivaBindingConstants; +import org.openhab.binding.emotiva.internal.EmotivaCommandHelper; +import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; + +/** + * Unit tests for EmotivaControl requests. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaControlRequestTest { + + private static Stream channelToDTOs() { + return Stream.of(Arguments.of(CHANNEL_STANDBY, OnOffType.ON, standby, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_STANDBY, OnOffType.OFF, standby, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MAIN_ZONE_POWER, OnOffType.ON, power_on, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MAIN_ZONE_POWER, OnOffType.OFF, power_off, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SOURCE, new StringType("HDMI1"), hdmi1, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SOURCE, new StringType("SHIELD"), source_2, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SOURCE, new StringType("hdmi1"), hdmi1, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SOURCE, new StringType("coax1"), coax1, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SOURCE, new StringType("NOT_REAL"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU, new StringType("0"), menu, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_CONTROL, new StringType("0"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_CONTROL, new StringType("MENU"), menu, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_CONTROL, new StringType("ENTER"), enter, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_CONTROL, new StringType("UP"), up, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_CONTROL, new StringType("DOWN"), down, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_CONTROL, new StringType("LEFT"), left, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_CONTROL, new StringType("RIGHT"), right, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_UP, new StringType("0"), up, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_DOWN, new StringType("0"), down, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_LEFT, new StringType("0"), left, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_RIGHT, new StringType("0"), right, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MENU_ENTER, new StringType("0"), enter, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MUTE, OnOffType.ON, mute_on, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MUTE, OnOffType.OFF, mute_off, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_DIM, OnOffType.ON, dim, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_DIM, OnOffType.OFF, dim, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE, new StringType("mode_ref_stereo"), reference_stereo, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE, new StringType("surround_mode"), surround_mode, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE, new StringType("mode_surround"), surround_mode, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE, new StringType("surround"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE, new StringType("1"), mode_up, PROTOCOL_V2, "1"), + Arguments.of(CHANNEL_MODE, new DecimalType(-1), mode_down, PROTOCOL_V2, "-1"), + Arguments.of(CHANNEL_MODE, OnOffType.ON, none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE, new DecimalType(1), mode_up, PROTOCOL_V2, "1"), + Arguments.of(CHANNEL_MODE, new DecimalType(-10), mode_down, PROTOCOL_V2, "-1"), + Arguments.of(CHANNEL_CENTER, new QuantityType<>(10, Units.DECIBEL), center_trim_set, PROTOCOL_V2, + "20.0"), + Arguments.of(CHANNEL_CENTER, new QuantityType<>(10, Units.DECIBEL), center_trim_set, PROTOCOL_V3, + "20.0"), + Arguments.of(CHANNEL_CENTER, new DecimalType(-30), center_trim_set, PROTOCOL_V2, "-24.0"), + Arguments.of(CHANNEL_CENTER, new DecimalType(-30), center_trim_set, PROTOCOL_V3, "-24.0"), + Arguments.of(CHANNEL_SUBWOOFER, new DecimalType(1), subwoofer_trim_set, PROTOCOL_V2, "2.0"), + Arguments.of(CHANNEL_SUBWOOFER, new DecimalType(1), subwoofer_trim_set, PROTOCOL_V3, "2.0"), + Arguments.of(CHANNEL_SUBWOOFER, new DecimalType(-25), subwoofer_trim_set, PROTOCOL_V2, "-24.0"), + Arguments.of(CHANNEL_SUBWOOFER, new DecimalType(-25), subwoofer_trim_set, PROTOCOL_V3, "-24.0"), + Arguments.of(CHANNEL_SURROUND, new DecimalType(30), surround_trim_set, PROTOCOL_V2, "24.0"), + Arguments.of(CHANNEL_SURROUND, new DecimalType(30), surround_trim_set, PROTOCOL_V3, "24.0"), + Arguments.of(CHANNEL_SURROUND, new DecimalType(-3.5), surround_trim_set, PROTOCOL_V2, "-7.0"), + Arguments.of(CHANNEL_SURROUND, new DecimalType(-3), surround_trim_set, PROTOCOL_V3, "-6.0"), + Arguments.of(CHANNEL_BACK, new DecimalType(-3), back_trim_set, PROTOCOL_V2, "-6.0"), + Arguments.of(CHANNEL_BACK, new DecimalType(-3), back_trim_set, PROTOCOL_V3, "-6.0"), + Arguments.of(CHANNEL_BACK, new DecimalType(30), back_trim_set, PROTOCOL_V2, "24.0"), + Arguments.of(CHANNEL_BACK, new DecimalType(30), back_trim_set, PROTOCOL_V3, "24.0"), + Arguments.of(CHANNEL_MODE_SURROUND, new StringType("0"), surround_mode, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SPEAKER_PRESET, OnOffType.ON, speaker_preset, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SPEAKER_PRESET, OnOffType.OFF, speaker_preset, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SPEAKER_PRESET, new StringType("preset2"), preset2, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SPEAKER_PRESET, new StringType("1"), speaker_preset, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SPEAKER_PRESET, new StringType("speaker_preset"), speaker_preset, PROTOCOL_V2, + "0"), + Arguments.of(CHANNEL_MAIN_VOLUME, new DecimalType(30), set_volume, PROTOCOL_V2, "15.0"), + Arguments.of(CHANNEL_MAIN_VOLUME, new PercentType("50"), set_volume, PROTOCOL_V2, "-41"), + Arguments.of(CHANNEL_MAIN_VOLUME_DB, new QuantityType<>(-96, Units.DECIBEL), set_volume, PROTOCOL_V2, + "-96.0"), + Arguments.of(CHANNEL_MAIN_VOLUME_DB, new QuantityType<>(-100, Units.DECIBEL), set_volume, PROTOCOL_V2, + "-96.0"), + Arguments.of(CHANNEL_LOUDNESS, OnOffType.ON, loudness_on, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_LOUDNESS, OnOffType.OFF, loudness_off, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_POWER, OnOffType.ON, zone2_power_on, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_POWER, OnOffType.OFF, zone2_power_off, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_VOLUME, new DecimalType(30), zone2_set_volume, PROTOCOL_V2, "15.0"), + Arguments.of(CHANNEL_ZONE2_VOLUME, new PercentType("50"), zone2_set_volume, PROTOCOL_V2, "-41"), + Arguments.of(CHANNEL_ZONE2_VOLUME_DB, new QuantityType<>(-96, Units.DECIBEL), zone2_set_volume, + PROTOCOL_V2, "-96.0"), + Arguments.of(CHANNEL_ZONE2_VOLUME_DB, new QuantityType<>(-100, Units.DECIBEL), zone2_set_volume, + PROTOCOL_V2, "-96.0"), + Arguments.of(CHANNEL_ZONE2_MUTE, OnOffType.ON, zone2_mute_on, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_MUTE, OnOffType.OFF, zone2_mute_off, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_SOURCE, new StringType("HDMI1"), hdmi1, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_SOURCE, new StringType("SHIELD"), source_2, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_SOURCE, new StringType("hdmi1"), hdmi1, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_SOURCE, new StringType("coax1"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_SOURCE, new StringType("zone2_coax1"), zone2_coax1, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_SOURCE, new StringType("zone2_ARC"), zone2_ARC, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_SOURCE, new StringType("NOT_REAL"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_ZONE2_SOURCE, new StringType("zone2_follow_main"), zone2_follow_main, PROTOCOL_V2, + "0"), + Arguments.of(CHANNEL_FREQUENCY, UpDownType.UP, frequency, PROTOCOL_V2, "1"), + Arguments.of(CHANNEL_FREQUENCY, UpDownType.DOWN, frequency, PROTOCOL_V2, "-1"), + Arguments.of(CHANNEL_SEEK, UpDownType.UP, seek, PROTOCOL_V2, "1"), + Arguments.of(CHANNEL_SEEK, UpDownType.DOWN, seek, PROTOCOL_V2, "-1"), + Arguments.of(CHANNEL_CHANNEL, UpDownType.UP, channel, PROTOCOL_V2, "1"), + Arguments.of(CHANNEL_CHANNEL, UpDownType.DOWN, channel, PROTOCOL_V2, "-1"), + Arguments.of(CHANNEL_TUNER_BAND, new StringType("band_am"), band_am, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_BAND, new StringType("band_fm"), band_fm, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_CHANNEL, new StringType("FM 107.90MHz"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_CHANNEL, QuantityType.valueOf(103000000, Units.HERTZ), none, PROTOCOL_V2, + "0"), + Arguments.of(CHANNEL_TUNER_CHANNEL, new StringType("channel_1"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_CHANNEL_SELECT, new StringType("channel_1"), channel_1, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_CHANNEL_SELECT, new StringType("CHANNEL_2"), channel_2, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_CHANNEL_SELECT, new StringType("FM 107.90MHz"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_CHANNEL_SELECT, QuantityType.valueOf(103000000, Units.HERTZ), none, + PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_SIGNAL, new StringType("Mono 0dBuV"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_PROGRAM, new StringType("Black Metal"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TUNER_RDS, new StringType("The Zombie Apocalypse is upon us!"), none, PROTOCOL_V2, + "0"), + Arguments.of(CHANNEL_AUDIO_INPUT, new StringType("HDMI 1"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_AUDIO_BITSTREAM, new StringType("HDMI 1"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_AUDIO_BITS, new StringType("PCM 5.1"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_VIDEO_INPUT, new StringType("HDMI 1"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_VIDEO_FORMAT, new StringType("1080P/60"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_VIDEO_SPACE, new StringType("RGB 8bits"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_INPUT1, new StringType("HDMI1"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_INPUT2, new StringType("HDMI2"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_INPUT3, new StringType("HDMI3"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_INPUT4, new StringType("HDMI4"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_INPUT5, new StringType("HDMI5"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_INPUT6, new StringType("HDMI6"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_INPUT7, new StringType("HDMI7"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_INPUT8, new StringType("HDMI8"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_REF_STEREO, new StringType("0"), reference_stereo, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_REF_STEREO, new StringType("0"), reference_stereo, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_REF_STEREO, REFRESH, none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_REF_STEREO, REFRESH, none, PROTOCOL_V3, "0"), + Arguments.of(CHANNEL_MODE_STEREO, new StringType("0"), stereo, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_MUSIC, new StringType("0"), music, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_MOVIE, new StringType("0"), movie, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_DIRECT, new StringType("0"), direct, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_DOLBY, new StringType("0"), dolby, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_DTS, new StringType("0"), dts, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_ALL_STEREO, new StringType("0"), all_stereo, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_MODE_AUTO, new StringType("0"), auto, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SELECTED_MODE, new StringType("Auto"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_SELECTED_MOVIE_MUSIC, new StringType("Surround"), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TREBLE, new DecimalType(0.5), treble_up, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TREBLE, new DecimalType(-1), treble_up, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_TREBLE, new DecimalType(0.5), treble_up, PROTOCOL_V3, "0"), + Arguments.of(CHANNEL_TREBLE, new DecimalType(-4), treble_down, PROTOCOL_V3, "0"), + Arguments.of(CHANNEL_BASS, new QuantityType<>(0, Units.DECIBEL), none, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_BASS, new QuantityType<>(-1, Units.DECIBEL), bass_down, PROTOCOL_V2, "0"), + Arguments.of(CHANNEL_BASS, new QuantityType<>(0, Units.DECIBEL), none, PROTOCOL_V3, "0"), + Arguments.of(CHANNEL_BASS, new QuantityType<>(-1, Units.DECIBEL), bass_down, PROTOCOL_V3, "0"), + Arguments.of(CHANNEL_WIDTH, new DecimalType(30), width_trim_set, PROTOCOL_V2, "24.0"), + Arguments.of(CHANNEL_WIDTH, new DecimalType(30), width_trim_set, PROTOCOL_V3, "24.0"), + Arguments.of(CHANNEL_WIDTH, new QuantityType<>(-1, Units.DECIBEL), width_trim_set, PROTOCOL_V2, "-2.0"), + Arguments.of(CHANNEL_WIDTH, new QuantityType<>(-1, Units.DECIBEL), width_trim_set, PROTOCOL_V3, "-2.0"), + Arguments.of(CHANNEL_HEIGHT, new DecimalType(0.499999), height_trim_set, PROTOCOL_V2, "1.0"), + Arguments.of(CHANNEL_HEIGHT, new DecimalType(-1.00000000001), height_trim_set, PROTOCOL_V3, "-2.0"), + Arguments.of(CHANNEL_HEIGHT, new QuantityType<>(-1, Units.DECIBEL), height_trim_set, PROTOCOL_V2, + "-2.0"), + Arguments.of(CHANNEL_HEIGHT, new QuantityType<>(-1, Units.DECIBEL), height_trim_set, PROTOCOL_V3, + "-2.0")); + } + + private static final EnumMap MAP_SOURCES_MAIN_ZONE = new EnumMap<>( + EmotivaControlCommands.class); + private static final EnumMap MAP_SOURCES_ZONE_2 = new EnumMap<>( + EmotivaControlCommands.class); + private static final EnumMap CHANNEL_MAP = new EnumMap<>( + EmotivaControlCommands.class); + private static final EnumMap RADIO_BAND_MAP = new EnumMap<>( + EmotivaControlCommands.class); + private static final Map STATE_MAP = Collections.synchronizedMap(new HashMap<>()); + private static final Map> COMMAND_MAPS = new ConcurrentHashMap<>(); + + @BeforeAll + static void beforeAll() { + MAP_SOURCES_MAIN_ZONE.put(source_1, "HDMI 1"); + MAP_SOURCES_MAIN_ZONE.put(source_2, "SHIELD"); + MAP_SOURCES_MAIN_ZONE.put(hdmi1, "HDMI1"); + MAP_SOURCES_MAIN_ZONE.put(coax1, "Coax 1"); + COMMAND_MAPS.put(EmotivaBindingConstants.MAP_SOURCES_MAIN_ZONE, MAP_SOURCES_MAIN_ZONE); + + MAP_SOURCES_ZONE_2.put(source_1, "HDMI 1"); + MAP_SOURCES_ZONE_2.put(source_2, "SHIELD"); + MAP_SOURCES_ZONE_2.put(hdmi1, "HDMI1"); + MAP_SOURCES_ZONE_2.put(zone2_coax1, "Coax 1"); + MAP_SOURCES_ZONE_2.put(zone2_ARC, "Audio Return Channel"); + MAP_SOURCES_ZONE_2.put(zone2_follow_main, "Follow Main"); + COMMAND_MAPS.put(EmotivaBindingConstants.MAP_SOURCES_ZONE_2, MAP_SOURCES_ZONE_2); + + CHANNEL_MAP.put(channel_1, "Channel 1"); + CHANNEL_MAP.put(channel_2, "Channel 2"); + CHANNEL_MAP.put(channel_3, "My Radio Channel"); + COMMAND_MAPS.put(tuner_channel.getEmotivaName(), CHANNEL_MAP); + + RADIO_BAND_MAP.put(band_am, "AM"); + RADIO_BAND_MAP.put(band_fm, "FM"); + COMMAND_MAPS.put(tuner_band.getEmotivaName(), RADIO_BAND_MAP); + + STATE_MAP.put(CHANNEL_TREBLE, new DecimalType(-3)); + STATE_MAP.put(CHANNEL_TUNER_CHANNEL, new StringType("FM 87.50MHz")); + STATE_MAP.put(CHANNEL_FREQUENCY, QuantityType.valueOf(107.90, Units.HERTZ)); + } + + @ParameterizedTest + @MethodSource("channelToDTOs") + void createDTO(String channel, Command ohValue, EmotivaControlCommands controlCommand, + EmotivaProtocolVersion protocolVersion, String requestValue) { + EmotivaControlRequest controlRequest = EmotivaCommandHelper.channelToControlRequest(channel, COMMAND_MAPS, + protocolVersion); + + EmotivaControlDTO dto = controlRequest.createDTO(ohValue, STATE_MAP.get(channel)); + assertThat(dto.getCommands().size(), is(1)); + assertThat(dto.getCommands().get(0).getName(), is(controlCommand.name())); + assertThat(dto.getCommands().get(0).getValue(), is(requestValue)); + assertThat(dto.getCommands().get(0).getVisible(), is(nullValue())); + assertThat(dto.getCommands().get(0).getStatus(), is(nullValue())); + assertThat(dto.getCommands().get(0).getAck(), is(DEFAULT_CONTROL_ACK_VALUE)); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtilsTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtilsTest.java new file mode 100644 index 00000000000..01e4933ff2a --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtilsTest.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 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.emotiva.internal.protocol; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import javax.xml.bind.JAXBException; +import javax.xml.bind.UnmarshalException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.emotiva.internal.AbstractDTOTestBase; +import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyWrapper; + +/** + * Unit tests for Emotiva message marshalling and unmarshalling. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaXmlUtilsTest extends AbstractDTOTestBase { + + public EmotivaXmlUtilsTest() throws JAXBException { + } + + @Test + void testUnmarshallEmptyString() { + assertThrows(JAXBException.class, () -> xmlUtils.unmarshallToEmotivaDTO(""), "xml value is null or empty"); + } + + @Test + void testUnmarshallNotValidXML() { + assertThrows(UnmarshalException.class, () -> xmlUtils.unmarshallToEmotivaDTO("notXmlAtAll")); + } + + @Test + void testUnmarshallInstanceObject() throws JAXBException { + Object object = xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2KeepAlive); + + assertThat(object, instanceOf(EmotivaNotifyWrapper.class)); + } + + @Test + void testUnmarshallXml() throws JAXBException { + Object object = xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2KeepAlive); + + assertThat(object, instanceOf(EmotivaNotifyWrapper.class)); + } + + @Test + void testMarshallObjectWithoutXmlElements() { + String commands = xmlUtils.marshallEmotivaDTO(""); + assertThat(commands, is("")); + } + + @Test + void testMarshallNoValueDTO() { + EmotivaNotifyWrapper dto = new EmotivaNotifyWrapper(); + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString, not(containsString(""))); + assertThat(xmlAsString, containsString("")); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 33b950b9ec0..67d903323ce 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -129,6 +129,7 @@ org.openhab.binding.electroluxair org.openhab.binding.elerotransmitterstick org.openhab.binding.elroconnects + org.openhab.binding.emotiva org.openhab.binding.energenie org.openhab.binding.energidataservice org.openhab.binding.enigma2