From bbeb0f605d56207f27dd69296f127461f5d09c89 Mon Sep 17 00:00:00 2001 From: Espen Fossen Date: Thu, 28 Dec 2023 19:02:39 +0100 Subject: [PATCH] [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 | 186 +++++ bundles/org.openhab.binding.emotiva/pom.xml | 25 + .../src/main/feature/feature.xml | 9 + .../internal/EmotivaBindingConstants.java | 145 ++++ .../internal/EmotivaCommandHandler.java | 149 ++++ .../internal/EmotivaConfiguration.java | 53 ++ .../internal/EmotivaHandlerFactory.java | 70 ++ .../internal/EmotivaProcessorHandler.java | 734 ++++++++++++++++++ .../internal/EmotivaTranslationProvider.java | 66 ++ .../internal/EmotivaUdpBroadcastService.java | 191 +++++ .../internal/EmotivaUdpReceivingService.java | 224 ++++++ .../internal/EmotivaUdpSendingService.java | 218 ++++++ .../internal/InputStateOptionProvider.java | 98 +++ .../UnsupportedCommandTypeException.java | 36 + .../discovery/EmotivaDiscoveryService.java | 67 ++ .../internal/dto/AbstractJAXBElementDTO.java | 45 ++ .../internal/dto/AbstractNotificationDTO.java | 42 + .../emotiva/internal/dto/ControlDTO.java | 80 ++ .../emotiva/internal/dto/EmotivaAckDTO.java | 50 ++ .../internal/dto/EmotivaBarNotifyDTO.java | 138 ++++ .../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 | 100 +++ .../internal/dto/EmotivaNotifyWrapper.java | 48 ++ .../emotiva/internal/dto/EmotivaPingDTO.java | 39 + .../internal/dto/EmotivaPropertyDTO.java | 70 ++ .../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 + .../ElementNameToEmotivaProperty.java | 35 + .../internal/protocol/EmotivaCommandType.java | 41 + .../protocol/EmotivaControlCommands.java | 239 ++++++ .../protocol/EmotivaControlRequest.java | 481 ++++++++++++ .../internal/protocol/EmotivaDataType.java | 57 ++ .../protocol/EmotivaPropertyStatus.java | 37 + .../protocol/EmotivaProtocolVersion.java | 46 ++ .../protocol/EmotivaSubscriptionTags.java | 234 ++++++ .../internal/protocol/EmotivaUdpResponse.java | 34 + .../internal/protocol/EmotivaXmlUtils.java | 303 ++++++++ .../protocol/OHChannelToEmotivaCommand.java | 111 +++ .../src/main/resources/OH-INF/addon/addon.xml | 11 + .../main/resources/OH-INF/config/config.xml | 86 ++ .../resources/OH-INF/i18n/emotiva.properties | 146 ++++ .../OH-INF/i18n/emotiva_no.properties | 146 ++++ .../resources/OH-INF/thing/thing-types.xml | 433 +++++++++++ .../emotiva/internal/AbstractDTOTestBase.java | 295 +++++++ .../internal/EmotivaCommandHandlerTest.java | 128 +++ .../internal/dto/EmotivaAckDTOTest.java | 63 ++ .../internal/dto/EmotivaBarNotifyDTOTest.java | 50 ++ .../internal/dto/EmotivaCommandDTOTest.java | 74 ++ .../internal/dto/EmotivaControlDTOTest.java | 75 ++ .../dto/EmotivaMenuNotifyDTOTest.java | 64 ++ .../dto/EmotivaNotifyWrapperTest.java | 93 +++ .../internal/dto/EmotivaPingDTOTest.java | 66 ++ .../internal/dto/EmotivaPropertyDTOTest.java | 58 ++ .../dto/EmotivaSubscriptionRequestTest.java | 91 +++ .../dto/EmotivaSubscriptionResponseTest.java | 99 +++ .../dto/EmotivaTransponderDTOTest.java | 65 ++ .../dto/EmotivaUnsubscriptionTest.java | 64 ++ .../dto/EmotivaUpdateRequestTest.java | 55 ++ .../dto/EmotivaUpdateResponseTest.java | 96 +++ .../protocol/EmotivaControlRequestTest.java | 370 +++++++++ .../protocol/EmotivaXmlUtilsTest.java | 77 ++ bundles/pom.xml | 1 + 76 files changed, 8136 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/EmotivaCommandHandler.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/UnsupportedCommandTypeException.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/ElementNameToEmotivaProperty.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/i18n/emotiva_no.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/EmotivaCommandHandlerTest.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 a3a4786ac56f1..024b98f48f0f4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -95,6 +95,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 232adfed1394f..2775214afa1d3 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -466,6 +466,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 0000000000000..38d625e349232 --- /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 0000000000000..07ff263426387 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/README.md @@ -0,0 +1,186 @@ +# 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 | +| 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 | + + +## Channels + +The Emotiva Processor supports the following channels (some channels are model specific): + +| Channel Type ID | Item Type | Description | +|-----------------------------|--------------|------------------------------------------------------------| +| _Main zone_ | | | +| mainZone#power | Switch (RW) | Main zone power on/off | +| mainZone#volume | Dimmer (RW) | Main zone volume | +| mainZone#volumeDB | Number (RW) | Main zone volume in dB (-96 to 15) | +| mainZone#mute | Switch (RW) | Main zone mute | +| mainZone#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 | +| zone2#volumeDB | 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 (RW) | 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#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 | +| menu_display_highlight | String (R) | Menu Panel Display: Value in focus | +| menu_display_top_start | String (R) | Menu Panel Display: Top bar, start cell | +| menu_display_top_center | String (R) | Menu Panel Display: Top bar, center cell | +| menu_display_top_end | String (R) | Menu Panel Display: Top bar, end cell | +| menu_display_middle_start | String (R) | Menu Panel Display: Middle bar, start cell | +| menu_display_middle_center | String (R) | Menu Panel Display: Middle bar, center cell | +| menu_display_middle_end | String (R) | Menu Panel Display: Middle bar, end cell | +| menu_display_bottom_start | String (R) | Menu Panel Display: Bottom bar, start cell | +| menu_display_bottom_center | String (R) | Menu Panel Display: Bottom bar, center cell | +| menu_display_bottom_end | String (R) | Menu Panel Display: Bottom bar, end cell | + +(R) = read-only (no updates possible) +(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:mainZone#volume"} +Number:Dimensionless emotiva_volume_db "Volume [%d dB]" {channel="emotiva:processor:1:mainzone#volume_db"} +Switch emotiva_mute "Mute" {channel="emotiva:processor:1:mainZone#mute"} +String emotiva_source "Source [%s]" {channel="emotiva:processor:1:mainZone#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 0000000000000..2b56b81172929 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/pom.xml @@ -0,0 +1,25 @@ + + + + 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 + + + + org.assertj + assertj-core + 3.25.2 + test + + + 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 0000000000000..df087808cb38d --- /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 0000000000000..18cc8a5fd92e1 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaBindingConstants.java @@ -0,0 +1,145 @@ +/** + * 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. */ + public static final String UNIQUE_PROPERTY_NAME = "ip4Address"; + + /** Default port used to discover Emotiva devices. */ + public static final int DEFAULT_PING_PORT = 7000; + + /** Default port used to receive transponder (discovered) Emotiva devices. */ + public static final int DEFAULT_TRANSPONDER_PORT = 7001; + + public static final int EMOTIVA_DEFAULT_SENDING_TIMEOUT = 1000; + + /** Thing is set OFFLINE after so many communication errors. A retry job is then started to try again */ + static final int CONNECTION_RETRIES = 3; + static final int DEFAULT_RETRY_INTERVAL_MINUTES = 2; + static int DEFAULT_KEEP_ALIVE_IN_MILLISECONDS = 7500; + static int DEFAULT_KEEP_ALIVE_CONSIDERED_LOST_IN_MILLISECONDS = 30000; + + // List of all Thing Type UIDs + public 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 communication */ + public static final String DEFAULT_CONTROL_MESSAGE_SET_DEFAULT_VALUE = "0"; + public static final String DEFAULT_SUBSCRIPTION_PROPERTY_ACK = "yes"; + + /* Default values for Emotiva channels */ + public static final int DEFAULT_TRIM_MIN_DECIBEL = -12; + public static final int DEFAULT_TRIM_MAX_DECIBEL = 12; + public static int DEFAULT_VOLUME_MIN_DECIBEL = -96; + public static int DEFAULT_VOLUME_MAX_DECIBEL = 15; + public static final String NAME_SOURCES_MAP = "sources"; + + /* Protocol V1 channels */ + public static final String CHANNEL_STANDBY = "general#standby"; + public static final String CHANNEL_MAIN_ZONE_POWER = "mainZone#power"; + public static final String CHANNEL_SOURCE = "mainZone#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 = "mainZone#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 = "mainZone#volume"; + public static final String CHANNEL_MAIN_VOLUME_DB = "mainZone#volume_db"; + public static final String CHANNEL_LOUDNESS = "general#loudness"; + public static final String CHANNEL_ZONE2_POWER = "zone2#zone2_power"; + public static final String CHANNEL_ZONE2_VOLUME = "zone2#zone2_volume"; + public static final String CHANNEL_ZONE2_VOLUME_DB = "zone2#zone2_volume_db"; + public static final String CHANNEL_ZONE2_MUTE = "zone2#zone2_mute"; + public static final String CHANNEL_ZONE2_INPUT = "zone2#zone2_input"; + 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_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"; + + /* 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"; + + /* 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_KEEP_ALIVE = "general#keep_alive"; + 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"; + + /* Miscellaneous Constants */ + public static final int PROTOCOL_V3_LEVEL_MULTIPLIER = 2; + public static final String TRIM_SET_COMMAND_SUFFIX = "_trim_set"; + public static final String MENU_PANEL_CHECKBOX_ON = "on"; + public static final String MENU_PANEL_HIGHLIGHTED = "true"; +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaCommandHandler.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaCommandHandler.java new file mode 100644 index 0000000000000..ddff325478091 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaCommandHandler.java @@ -0,0 +1,149 @@ +/** + * 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_VOLUME_MAX_DECIBEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_VOLUME_MIN_DECIBEL; + +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.DecimalType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.Command; + +/** + * A command handler translates an openHAB command into an Emotiva commands. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class EmotivaCommandHandler { + + private final EmotivaConfiguration config; + + public EmotivaCommandHandler(EmotivaConfiguration config) { + this.config = config; + } + + public int emotivaVolumeValue(Command command) throws UnsupportedCommandTypeException { + return emotivaVolumeValue(command, DEFAULT_VOLUME_MIN_DECIBEL, DEFAULT_VOLUME_MAX_DECIBEL); + } + + public int emotivaVolumeValue(Command command, int minValue, int maxValue) throws UnsupportedCommandTypeException { + + if (command == IncreaseDecreaseType.INCREASE) { + return +1; + } else if (command == IncreaseDecreaseType.DECREASE) { + return -1; + } else if (command instanceof PercentType percentCommand) { + return (percentCommand.intValue() * (config.getMainVolumeMax().intValue() - minValue) / 100) + minValue; + } else if (command instanceof DecimalType decimalCommand) { + if (decimalCommand.intValue() > maxValue) { + return maxValue; + } else if (decimalCommand.intValue() < minValue) { + return minValue; + } + return decimalCommand.intValue(); + } else if (command instanceof QuantityType quantityType) { + return quantityType.intValue(); + } else { + throw new UnsupportedCommandTypeException(); + } + } + + 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(0 - 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; // progress bar width in chars + 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 0000000000000..0edd50c834c62 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaConfiguration.java @@ -0,0 +1,53 @@ +/** + * 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_KEEP_ALIVE_IN_MILLISECONDS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_RETRY_INTERVAL_MINUTES; + +import java.math.BigDecimal; + +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 static final BigDecimal MAX_VOLUME = new BigDecimal("15"); + + 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 model = ""; + public String revision = ""; + public String dataRevision = ""; + public String protocolVersion = "2.0"; + public int keepAlive = DEFAULT_KEEP_ALIVE_IN_MILLISECONDS; + public int retryConnectInMinutes = DEFAULT_RETRY_INTERVAL_MINUTES; + private BigDecimal mainVolumeMax = MAX_VOLUME; + + public BigDecimal getMainVolumeMax() { + return mainVolumeMax; + } + + public void setMainVolumeMax(BigDecimal mainVolumeMax) { + this.mainVolumeMax = mainVolumeMax; + } +} 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 0000000000000..e218ae591a452 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaHandlerFactory.java @@ -0,0 +1,70 @@ +/** + * 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 EmotivaHandlerFactory} 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); + + @Activate + public EmotivaHandlerFactory(final @Reference EmotivaTranslationProvider 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); + } 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 0000000000000..caf6c815c011d --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorHandler.java @@ -0,0 +1,734 @@ +/** + * 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.CHANNEL_BAR; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT1; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_KEEP_ALIVE; +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_DISPLAY_HIGHLIGHT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_DISPLAY_PREFIX; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MUTE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_CHANNEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_MUTE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME_DB; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CONNECTION_RETRIES; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_KEEP_ALIVE_CONSIDERED_LOST_IN_MILLISECONDS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_KEEP_ALIVE_IN_MILLISECONDS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MENU_PANEL_CHECKBOX_ON; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MENU_PANEL_HIGHLIGHTED; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.NAME_SOURCES_MAP; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHandler.channelToControlRequest; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHandler.getMenuPanelColumnLabel; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHandler.getMenuPanelRowLabel; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHandler.updateProgress; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHandler.volumeDecibelToPercentage; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHandler.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.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.Objects; +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 {@link 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 sources; + private final EnumMap modes; + private final Map> commandMaps = new ConcurrentHashMap<>(); + + 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(EmotivaBindingConstants.BINDING_ID, + true); + + private final EmotivaXmlUtils xmlUtils = new EmotivaXmlUtils(); + + private boolean udpSenderActive = false; + + public EmotivaProcessorHandler(Thing thing) throws JAXBException { + super(thing); + this.config = getConfigAs(EmotivaConfiguration.class); + this.retryConnectInMinutes = config.retryConnectInMinutes; + sources = new EnumMap<>(EmotivaControlCommands.class); + EnumMap channels = new EnumMap<>( + Map.ofEntries(Map.entry(EmotivaControlCommands.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()))); + EnumMap bands = new EnumMap<>( + Map.of(band_am, band_am.getLabel(), band_fm, band_fm.getLabel())); + modes = new EnumMap<>(EmotivaSubscriptionTags.class); + commandMaps.put(NAME_SOURCES_MAP, sources); + commandMaps.put(tuner_channel.getName(), channels); + commandMaps.put(tuner_band.getName(), bands); + } + + @Override + public void initialize() { + logger.info("initialize: {}", getThing().getUID()); + + scheduler.execute(this::connect); + } + + private synchronized void connect() { + final EmotivaConfiguration localConfig = config; + try { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/message.processor.connecting"); + + 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); + + for (int attempt = 1; attempt <= CONNECTION_RETRIES && !udpSenderActive; attempt++) { + try { + logger.debug("Attempt {}", attempt); + sendConnector.sendSubscription(generalSubscription, config); + sendConnector.sendSubscription(nonGeneralSubscriptions, config); + // TODO Request bar_update for volume, will give user specified max volume + + } catch (IOException e) { + // network or socket failure, also wait 2 sec and try again + } + + // answer expected within 50-600ms on a regular network; wait up to 2sec just to make sure + 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); + } + + private void startPollingKeepAlive() { + final ScheduledFuture localRefreshJob = this.pollingJob; + if (localRefreshJob == null || localRefreshJob.isCancelled()) { + logger.debug("Start polling"); + // Add the DEFAULT_KEEP_ALIVE_IN_MILLISECONDS as a time buffer for checking, to avoid flapping state or + // minor network issues + 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(CHANNEL_KEEP_ALIVE); + 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"); + // 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()); + // TODO: Check if is working + } + } + + 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()); + return; + } + + if (object instanceof EmotivaAckDTO answerDto) { + logger.debug("Processing received {} with {} ", EmotivaAckDTO.class.getSimpleName(), + emotivaUdpResponse.answer()); + // TODO: Maybe revert channel state change if answer is "nak" + } else if (object instanceof EmotivaBarNotifyWrapper answerDto) { + logger.debug("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.debug("Processing received {} with {} ", EmotivaNotifyWrapper.class.getSimpleName(), + emotivaUdpResponse.answer()); + handleNotificationUpdate(answerDto); + } else if (object instanceof EmotivaUpdateResponse answerDto) { + logger.debug("Processing received {} with {} ", EmotivaUpdateResponse.class.getSimpleName(), + emotivaUdpResponse.answer()); + handleNotificationUpdate(answerDto); + } else if (object instanceof EmotivaMenuNotifyDTO answerDto) { + logger.debug("Processing received {} with {} ", EmotivaMenuNotifyDTO.class.getSimpleName(), + emotivaUdpResponse.answer()); + + if (answerDto.getRow() != null) { + handleMenuNotify(answerDto); + } else if (answerDto.getProgress() != null && answerDto.getProgress().getTime() != null) { + logger.debug("Processing received {} with {} ", EmotivaMenuNotifyDTO.class.getSimpleName(), + emotivaUdpResponse.answer()); + listeningThreadFactory + .newThread(() -> handleMenuNotifyProgressMessage(answerDto.getProgress().getTime())).start(); + } + + } else if (object instanceof EmotivaSubscriptionResponse answerDto) { + logger.debug("Processing received {} with {} ", EmotivaSubscriptionResponse.class.getSimpleName(), + emotivaUdpResponse.answer()); + + // Populates static input sources, except input + sources.putAll(EmotivaControlCommands.getCommandsFromType(EmotivaCommandType.SOURCE)); + sources.remove(EmotivaControlCommands.input); + commandMaps.put(NAME_SOURCES_MAP, sources); + + if (answerDto.getProperties() == null) { + for (EmotivaNotifyDTO dto : xmlUtils.unmarshallToNotification(answerDto.getTags())) { + // TODO: Not all message types needs to be processed during EmotivaSubscriptionResponse, like + // keepAlive and goodbye + handleChannel(dto.getName(), dto.getValue(), dto.getVisible(), dto.getAck()); + } + } else { + for (EmotivaPropertyDTO property : answerDto.getProperties()) { + handleChannel(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(EmotivaCommandHandler.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) { + if (sendingService != null) { + try { + sendingService.sendUpdate(tags, config); + } 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()) { + handleChannel(property.getName(), property.getValue(), property.getVisible(), property.getStatus()); + } + } + } + + private void handleChannel(String name, String value, String visible, String status) { + + if (Objects.nonNull(status) && status.equals(NOT_VALID.name())) { + logger.debug("Subscription property {} not present in device, skipping", name); + return; + } + + if (Objects.nonNull(value) && "None".equals(value)) { + logger.debug("No value present for channel {}, usually means a speaker is not enabled", name); + return; + } + + try { + EmotivaSubscriptionTags.hasChannel(name); + } catch (IllegalArgumentException e) { + logger.debug("Subscription property {} is not know to the binding, might need updating", name); + return; + } + + if (Objects.nonNull(status) && noSubscriptionToChannel().contains(EmotivaSubscriptionTags.valueOf(name))) { + logger.debug("Initial subscription status update for {}, skipping, only want notifications", name); + return; + } + + try { + EmotivaSubscriptionTags subscriptionTag; + try { + subscriptionTag = EmotivaSubscriptionTags.valueOf(name); + } catch (IllegalArgumentException e) { + logger.debug("Property {} could not be mapped subscription tag, skipping", name); + return; + } + + if (subscriptionTag.getChannel().isEmpty()) { + logger.debug("Subscription property {} does not have a corresponding channel, skipping", name); + return; + } + + // 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", value); + sources.put(EmotivaControlCommands.matchToInput(subscriptionTag.name()), value); + commandMaps.put(NAME_SOURCES_MAP, sources); + } + + // Add/Update audio modes + if (subscriptionTag.getChannel().startsWith(CHANNEL_MODE + "_") && "true".equals(visible)) { + logger.debug("Adding {} to dynamic mode list", value); + modes.put(EmotivaSubscriptionTags.fromChannelUID(subscriptionTag.getChannel()), value); + } + + findChannelDatatypeAndUpdateChannel(subscriptionTag.getChannel(), value, subscriptionTag.getDataType()); + } catch (IllegalArgumentException e) { + logger.debug("Error updating subscription property {}", name, 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 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); + } + } + + private void updateChannelState(String channelID, State state) { + stateMap.put(channelID, state); + // TODO: Consider checking previous to new value + logger.debug("Updating channel {} with {} ", 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()); + + if (sendingService != 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())); + sendingService.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)) { + sendingService.sendUpdate(EmotivaSubscriptionTags.speakerChannels(), config); + } + } + + } 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 connector = sendingService; + if (connector != null) { + logger.debug("Disposing active sender"); + if (udpSenderActive) { + try { + // Unsubscribe before disconnect + sendingService.sendUnsubscribe(generalSubscription); + sendingService.sendUnsubscribe(nonGeneralSubscriptions); + } catch (IOException e) { + logger.debug("Failed to unsubscribe for: {}", config.ipAddress, e); + } + } + + sendingService = null; + try { + connector.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 getSources() { + return sources; + } + + 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 0000000000000..21f92ac56cbf5 --- /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 0000000000000..9491375e69efe --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpBroadcastService.java @@ -0,0 +1,191 @@ +/** + * 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.EMOTIVA_DEFAULT_SENDING_TIMEOUT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.THING_PROCESSOR; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V3; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +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. + startDiscoverSocket(); + // 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 { + discoverSocket.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 (IOException e) { + logger.debug("Error during discovery: {}", e.getMessage()); + } finally { + closeDiscoverSocket(); + } + return Optional.empty(); + } + + /** + * Opens a {@link DatagramSocket} and sends a packet for discovery of Emotiva devices. + * + * @throws SocketException Error creating UDP socket + * @throws UnknownHostException Could not send message on socket + */ + private void startDiscoverSocket() throws IOException { + discoverSocket = new DatagramSocket(new InetSocketAddress(EmotivaBindingConstants.DEFAULT_TRANSPONDER_PORT)); + discoverSocket.setBroadcast(true); + discoverSocket.setSoTimeout(EMOTIVA_DEFAULT_SENDING_TIMEOUT); + final InetAddress broadcast = InetAddress.getByName(broadcastAddress); + + // TODO: Check what happens if we check with PROTOCOL_V2 + 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); + discoverSocket.send(discoverPacket); + if (logger.isTraceEnabled()) { + logger.trace("Discovery package sent: {}", new String(discoverPacket.getData(), StandardCharsets.UTF_8)); + } + } + + /** + * Closes the discovery socket and cleans the value. No need for synchronization as this method is called from a + * synchronized context. + */ + public void closeDiscoverSocket() { + if (discoverSocket != null) { + discoverSocket.close(); + discoverSocket = null; + } + } + + /** + * 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.debug("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(), "2.0")) + .withProperty("dataRevision", Objects.requireNonNullElse(answerDto.getDataRevision(), "1.0")) + .withProperty("protocolVersion", + Objects.requireNonNullElse(answerDto.getControl().getVersion(), "2.0")) + .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 0000000000000..21c2bbc741eea --- /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.info("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.info("Socket closed; stopping listener on port {}.", receivingPort); + } else { + logger.info("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.CONNECTION_RETRIES) { + logger.info( + "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.info("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.debug("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.info("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 0000000000000..3d47027965879 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpSendingService.java @@ -0,0 +1,218 @@ +/** + * 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.EMOTIVA_DEFAULT_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.debug("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.info("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.info("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(EMOTIVA_DEFAULT_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"); + } + } + } + + public boolean isConnected() { + return sendingSocket != null; + } +} 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 0000000000000..8a06f439d57a4 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/InputStateOptionProvider.java @@ -0,0 +1,98 @@ +/** + * 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 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 the input channel of a source. + * + * @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<>(); + if (handler != null) { + if (channel.getUID().getId().equals(CHANNEL_SOURCE)) { + EnumMap sources = handler.getSources(); + Collection sourceKeys = sources.keySet(); + for (EmotivaControlCommands sourceKey : sourceKeys) { + if (sourceKey.name().startsWith("source_")) { + options.add(new StateOption(sourceKey.name(), sources.get(sourceKey))); + } else { + options.add(new StateOption(sourceKey.name(), sourceKey.getLabel())); + } + } + setStateOptions(channel.getUID(), options); + logger.debug("Updated {} with {} options", CHANNEL_SOURCE, options.size()); + } else if (channel.getUID().getId().equals(CHANNEL_MODE)) { + EnumMap modes = handler.getModes(); + Collection modeKeys = modes.keySet(); + for (EmotivaSubscriptionTags modeKey : modeKeys) { + options.add(new StateOption(modeKey.name(), modes.get(modeKey))); + } + setStateOptions(channel.getUID(), options); + logger.debug("Updated {} with {} options", CHANNEL_MODE, options.size()); + } + } + + return super.getStateDescription(channel, original, locale); + } +} diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/UnsupportedCommandTypeException.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/UnsupportedCommandTypeException.java new file mode 100644 index 0000000000000..c08d5a3af3055 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/UnsupportedCommandTypeException.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 org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when an unsupported command type is sent to a channel. + * + * @author Jan-Willem Veldhuis - Initial contribution + * @author Espen Fossen - Adapted to Emotiva binding + * + */ +@NonNullByDefault +public class UnsupportedCommandTypeException extends Exception { + + private static final long serialVersionUID = 42L; + + public UnsupportedCommandTypeException() { + super(); + } + + public UnsupportedCommandTypeException(String message) { + super(message); + } +} 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 0000000000000..be1f8f83957f7 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/discovery/EmotivaDiscoveryService.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.discovery; + +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.SUPPORTED_THING_TYPES_UIDS; + +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 - Adapted to Emotiva binding + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, configurationPid = "discovery.emotiva") +public class EmotivaDiscoveryService extends AbstractDiscoveryService { + + private final Logger logger = LoggerFactory.getLogger(EmotivaDiscoveryService.class); + private static final int DISCOVERY_TIMEOUT_SECONDS = 5; + private static final String BROADCAST_ADDRESS = "255.255.255.255"; + @Nullable + private final EmotivaUdpBroadcastService broadcastService = new EmotivaUdpBroadcastService(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."); + if (broadcastService != null) { + try { + broadcastService.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 0000000000000..8aed2c488c745 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/AbstractJAXBElementDTO.java @@ -0,0 +1,45 @@ +/** + * 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; + +/** + * 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; + } +} 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 0000000000000..52845f446cc9d --- /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=2.x or older + @XmlAnyElement(lax = true) + List tags; + + // Only present with protocol=3.x 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 0000000000000..ed6de5ae444c2 --- /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 0000000000000..5c77dc5d66d99 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTO.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.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 EmotivaAckDTO(List commands) { + this.commands = commands; + } + + public List getCommands() { + return commands; + } + + public void setCommands(List commands) { + this.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 0000000000000..9b5d559f4037e --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTO.java @@ -0,0 +1,138 @@ +/** + * 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; + +import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags; + +/** + * The EmotivaBarNotify message type. Received from a device if subscribed to the + * {@link 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 0000000000000..eb35615abc9b0 --- /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 0000000000000..86cd6e9c12df5 --- /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_SUBSCRIPTION_PROPERTY_ACK; + +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_SUBSCRIPTION_PROPERTY_ACK); + 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_SUBSCRIPTION_PROPERTY_ACK); + 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_SUBSCRIPTION_PROPERTY_ACK); + 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 0000000000000..3c4e7e1cb4e6f --- /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 0000000000000..1061680e27858 --- /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 0000000000000..24d0cad750f09 --- /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 0000000000000..f068ffb6dfbf1 --- /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 0000000000000..8683c91fde95c --- /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 0000000000000..13ef6ad73c148 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyDTO.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 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.EmotivaSubscriptionTags; + +/** + * The EmotivaNotify message type. Received from a device if subscribed to {@link 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 EmotivaNotifyDTO(EmotivaSubscriptionTags tag, String value) { + this.tagName = tag.getName(); + this.value = value; + } + + public static EmotivaNotifyDTO fromType(EmotivaSubscriptionTags command) { + return new EmotivaNotifyDTO(command.name()); + } + + 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 0000000000000..b83889b956585 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapper.java @@ -0,0 +1,48 @@ +/** + * 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; + +import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils; + +/** + * 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 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 0000000000000..e1469ad8eda7d --- /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 0000000000000..11cac11a27a55 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTO.java @@ -0,0 +1,70 @@ +/** + * 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; + +/** + * 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 value; + } + + public String getVisible() { + return visible; + } + + public String getStatus() { + return 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 0000000000000..6bc153646c697 --- /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 0000000000000..faff31332029f --- /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 0000000000000..7aae71607c4b4 --- /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=2.x or older + @XmlAnyElement(lax = true) + List tags; + + // Only present with protocol=3.x 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 0000000000000..d31e65dd9c706 --- /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 0000000000000..6a97e53a364f0 --- /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 0000000000000..0fb3c9fc15650 --- /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 0000000000000..5b7b5a71fa5f0 --- /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/ElementNameToEmotivaProperty.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/ElementNameToEmotivaProperty.java new file mode 100644 index 0000000000000..ed54b107d3c25 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/ElementNameToEmotivaProperty.java @@ -0,0 +1,35 @@ +/** + * 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 javax.xml.bind.annotation.adapters.XmlAdapter; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Helper class to provide XML message element name as actual name of command. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class ElementNameToEmotivaProperty extends XmlAdapter { + @Override + public EmotivaControlCommands unmarshal(String command) { + return EmotivaControlCommands.valueOf(command); + } + + @Override + public String marshal(EmotivaControlCommands command) { + return command.toString(); + } +} 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 0000000000000..3b79cdd78dc19 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaCommandType.java @@ -0,0 +1,41 @@ +/** + * 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; +import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO; + +/** + * Enum types for commands to send to Emotiva devices. Used by {@link EmotivaControlRequest} to create correct + * {@link 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, // Input sources + SPEAKER_PRESET, // Speaker preset + TOGGLE, // Two state toggle + UP_DOWN_SINGLE, // +1/-1 + UP_DOWN_HALF, // +0.5/-0.5 + USER_SOURCE, // Source with possible user assigned name + ZONE2_SOURCE // Zone 2 input sources + +} 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 0000000000000..f9d6ec7e74717 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlCommands.java @@ -0,0 +1,239 @@ +/** + * 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; +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.EmotivaCommandType.USER_SOURCE; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.ZONE2_SOURCE; +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", USER_SOURCE, STRING), + source_1("Input 1", USER_SOURCE, STRING), + source_2("Input 2", USER_SOURCE, STRING), + source_3("Input 3", USER_SOURCE, STRING), + source_4("Input 4", USER_SOURCE, STRING), + source_5("Input 5", USER_SOURCE, STRING), + source_6("Input 6", USER_SOURCE, STRING), + source_7("Input 7", USER_SOURCE, STRING), + source_8("Input 8", USER_SOURCE, 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("", UP_DOWN_SINGLE, 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), + preset1("Preset 1", SET, STRING), + preset2("Preset 2", SET, STRING), + dirac("Dirac", SET, STRING), + hdmi1("HDMI 1", SOURCE, STRING), + hdmi2("HDMI 2", SOURCE, STRING), + hdmi3("HDMI 3", SOURCE, STRING), + hdmi4("HDMI 4", SOURCE, STRING), + hdmi5("HDMI 5", SOURCE, STRING), + hdmi6("HDMI 6", SOURCE, STRING), + hdmi7("HDMI 7", SOURCE, STRING), + hdmi8("HDMI 8", SOURCE, STRING), + coax1("Coax 1", SOURCE, STRING), + coax2("Coax 2", SOURCE, STRING), + coax3("Coax 3", SOURCE, STRING), + coax4("Coax 4", SOURCE, STRING), + optical1("Optical 1", SOURCE, STRING), + optical2("Optical 2", SOURCE, STRING), + optical3("Optical 3", SOURCE, STRING), + optical4("Optical 4", SOURCE, STRING), + arc("ARC", SOURCE, STRING), + usb_stream("USB Stream", SOURCE, STRING), + tuner("Tuner 1", SOURCE, STRING), + analog1("Analog 1", SOURCE, STRING), + analog2("Analog 2", SOURCE, STRING), + analog3("Analog 3", SOURCE, STRING), + analog4("Analog 4", SOURCE, STRING), + analog5("Analog 5", SOURCE, STRING), + analog71("Analog 7.1", SOURCE, STRING), + front_in("Front In", SOURCE, 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", ZONE2_SOURCE, STRING), + zone2_analog2("Analog 1", ZONE2_SOURCE, STRING), + zone2_analog3("Analog 1", ZONE2_SOURCE, STRING), + zone2_analog4("Analog 1", ZONE2_SOURCE, STRING), + zone2_analog5("Analog 1", ZONE2_SOURCE, STRING), + zone2_analog71("Analog 7.1", ZONE2_SOURCE, STRING), + zone2_analog8("Analog 8", ZONE2_SOURCE, STRING), + zone2_front_in("Front In", ZONE2_SOURCE, STRING), + zone2_arc("ARC", ZONE2_SOURCE, STRING), + zone2_ethernet("Ethernet", ZONE2_SOURCE, STRING), + zone2_follow_main("Follow Main", ZONE2_SOURCE, STRING), + zone2_coax1("Coax 1", ZONE2_SOURCE, STRING), + zone2_coax2("Coax 2", ZONE2_SOURCE, STRING), + zone2_coax3("Coax 3", ZONE2_SOURCE, STRING), + zone2_coax4("Coax 4", ZONE2_SOURCE, STRING), + zone2_optical1("Optical 1", ZONE2_SOURCE, STRING), + zone2_optical2("Optical 2", ZONE2_SOURCE, STRING), + zone2_optical3("Optical 3", ZONE2_SOURCE, STRING), + zone2_optical4("Optical 4", ZONE2_SOURCE, 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 0000000000000..d54c565dfc964 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequest.java @@ -0,0 +1,481 @@ +/** + * 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_SOURCE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_BAND; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_CHANNEL_SELECT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_TRIM_MAX_DECIBEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_TRIM_MIN_DECIBEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_VOLUME_MAX_DECIBEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_VOLUME_MIN_DECIBEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.NAME_SOURCES_MAP; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.PROTOCOL_V3_LEVEL_MULTIPLIER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.TRIM_SET_COMMAND_SUFFIX; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHandler.clamp; +import static org.openhab.binding.emotiva.internal.EmotivaCommandHandler.volumePercentageToDecibel; +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.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.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.getName(), 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(getDefaultCommand()); + } + } 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.info("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.getName()); + } + case CHANNEL_TUNER_CHANNEL_SELECT -> { + return matchToCommandMap(ohCommand, tuner_channel.getName()); + } + case CHANNEL_SOURCE -> { + return matchToCommandMap(ohCommand, NAME_SOURCES_MAP); + } + 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.info("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.info("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.info("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.info("Could not create EmotivaControlDTO for {}:{}:{}, ohCommand is {}", channel, name, + UP_DOWN_HALF, ohCommand.getClass().getSimpleName()); + 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); + for (EmotivaControlCommands command : commandMap.keySet()) { + if (commandMap.get(command).equals(value.toString())) { + return EmotivaControlDTO.create(EmotivaControlCommands.matchToInput(command.toString())); + } else if (command.name().equals(value.toString().toLowerCase())) { + 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()); + } + } + logger.info("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 0000000000000..f1a7ddb274abc --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaDataType.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.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("number_time"), + NOT_AVAILABLE("not_implemented"), + NOT_IMPLEMENTED("unknown"), + 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 0000000000000..77f99e2a40e33 --- /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 0000000000000..feb590df24901 --- /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 0000000000000..6b56cf858a995 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaSubscriptionTags.java @@ -0,0 +1,234 @@ +/** + * 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_AUDIO_BITS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_AUDIO_BITSTREAM; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_AUDIO_INPUT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_BACK; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_BAR; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_BASS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_CENTER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_DIM; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_HEIGHT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT1; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT2; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT3; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT4; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT5; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT6; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT7; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT8; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_KEEP_ALIVE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_LOUDNESS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MAIN_VOLUME; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MAIN_ZONE_POWER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_DISPLAY_PREFIX; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE; +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_SELECTED_MODE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SELECTED_MOVIE_MUSIC; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SOURCE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SPEAKER_PRESET; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SUBWOOFER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SURROUND; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TREBLE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_BAND; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_CHANNEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_PROGRAM; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_RDS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_SIGNAL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_VIDEO_FORMAT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_VIDEO_INPUT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_VIDEO_SPACE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_WIDTH; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_INPUT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_POWER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME; +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.FREQUENCY_HERTZ; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.GOODBYE; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.NUMBER_TIME; +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.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * 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_INPUT), + 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_DTS), + 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, CHANNEL_KEEP_ALIVE), + 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, ""); + + /* 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(keepAlive, goodBye); + } + + public String getName() { + return name; + } + + 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 0000000000000..0027c9fab0014 --- /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 0000000000000..71f4c2ee7839a --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtils.java @@ -0,0 +1,303 @@ +/** + * 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.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 marshallering and unmarshalling Emotiva message types. + * + * @author Espen Fossen - Initial contribution + */ +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)); + JAXBElement jaxbElement = new JAXBElement(name, String.class, null); + commandsAsJAXBElement.add(jaxbElement); + } + } + } + + // 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); + // TODO: Consider to filter out EmotivaControlCommands.none + commands.add(commandDTO); + } catch (IllegalArgumentException e) { + logger.info("Notify tag {} is unknown or not defined, skipping.", xmlElement.getTagName(), e); + } + } catch (ClassCastException e) { + logger.info("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.info("Notify tag {} is unknown or not defined, skipping.", xmlElement.getTagName(), e); + } + } catch (ClassCastException e) { + logger.info("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.info("Bar notify type {} is unknown or not defined, skipping.", xmlElement.getTagName(), e); + } + } catch (ClassCastException e) { + logger.info("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.info("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.info("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 0000000000000..9b35f722b7415 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/OHChannelToEmotivaCommand.java @@ -0,0 +1,111 @@ +/** + * 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_WIDTH; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_MUTE; +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), + 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), + 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), + mode_surround(CHANNEL_MODE_SURROUND, EmotivaControlCommands.surround); + + 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 0000000000000..046b316dc7582 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + Emotiva Binding + This is the binding for devices from the Emotiva Audio Corporation. + local + + 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 0000000000000..285fdc6b340a9 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,86 @@ + + + + + + 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 + + + model + + Device model name + + + revision + + Revision for given Emotiva device model + true + + + data-revision + + Data revision for given Emotiva device model + true + + + protocol-revision + + Protocol version, only change if you know what your doing + true + + + keep-alive + + The interval, in milliseconds, at which the Emotiva Device will send a "keepAlive" + notification + + 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 0000000000000..fec6dd0a48366 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/i18n/emotiva.properties @@ -0,0 +1,146 @@ +# add-on + +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.mainZone.label = Main Zone Control +thing-type.emotiva.processor.group.mainZone.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 zone2 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.model.label = Device Model +thing-type.config.emotiva.config.model.description = Model name of the Emotiva device +thing-type.config.emotiva.config.revision.label = Model Revision +thing-type.config.emotiva.config.revision.description = Revision of the device model +thing-type.config.emotiva.config.dataRevision.label = Data Revision +thing-type.config.emotiva.config.dataRevision.description = Data revision for the device model +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_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.speaker_preset.label = Speaker Preset +channel-type.emotiva.speaker_preset.description = Speaker Preset Name +channel-type.emotiva.speaker_preset.state.option.preset1 = Speaker Preset 1 +channel-type.emotiva.speaker_preset.state.option.preset2 = 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.option.channel_1 = Channel 1 +channel-type.emotiva.tuner_channel_select.option.channel_2 = Channel 2 +channel-type.emotiva.tuner_channel_select.option.channel_3 = Channel 3 +channel-type.emotiva.tuner_channel_select.option.channel_4 = Channel 4 +channel-type.emotiva.tuner_channel_select.option.channel_5 = Channel 5 +channel-type.emotiva.tuner_channel_select.option.channel_6 = Channel 6 +channel-type.emotiva.tuner_channel_select.option.channel_7 = Channel 7 +channel-type.emotiva.tuner_channel_select.option.channel_8 = Channel 8 +channel-type.emotiva.tuner_channel_select.option.channel_9 = Channel 9 +channel-type.emotiva.tuner_channel_select.option.channel_10 = Channel 10 +channel-type.emotiva.tuner_channel_select.option.channel_11 = Channel 11 +channel-type.emotiva.tuner_channel_select.option.channel_12 = Channel 12 +channel-type.emotiva.tuner_channel_select.option.channel_13 = Channel 13 +channel-type.emotiva.tuner_channel_select.option.channel_14 = Channel 14 +channel-type.emotiva.tuner_channel_select.option.channel_15 = Channel 15 +channel-type.emotiva.tuner_channel_select.option.channel_16 = Channel 16 +channel-type.emotiva.tuner_channel_select.option.channel_17 = Channel 17 +channel-type.emotiva.tuner_channel_select.option.channel_18 = Channel 18 +channel-type.emotiva.tuner_channel_select.option.channel_19 = Channel 19 +channel-type.emotiva.tuner_channel_select.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_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 = Failed to receive keepAlive message from device, check network connectivity +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/i18n/emotiva_no.properties b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/i18n/emotiva_no.properties new file mode 100644 index 0000000000000..07ca5ac5aee41 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/i18n/emotiva_no.properties @@ -0,0 +1,146 @@ +# add-on + +addon.emotiva.name = Emotiva Binding +addon.emotiva.description = Dette er en binding for Emotiva Audio Corporation AV prosessorer. + +# thing types + +thing-type.emotiva.processor.label = Prosessor +thing-type.emotiva.processor.description = Control a Emotiva AV Prosessor +thing-type.emotiva.processor.group.mainZone.label = Hovedsone +thing-type.emotiva.processor.group.mainZone.description = Kanaler for hovedsone til enheten +thing-type.emotiva.processor.group.zone2.label = Sone 2 +thing-type.emotiva.processor.group.zone2.description = Kanaler for sone 2 til enheten + +# thing types config + +thing-type.config.emotiva.config.ipAddress.label = IP adresse +thing-type.config.emotiva.config.ipAddress.description = IP adresse for enheten +thing-type.config.emotiva.config.controlPort = Kontrollport +thing-type.config.emotiva.config.controlPort.description = UDP port for å sende kommandoer til enheten +thing-type.config.emotiva.config.notifyPort.label = Endringsport +thing-type.config.emotiva.config.notifyPort.description = UDP port for endringer fra enheten +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 fjernstyrt configurasjon +thing-type.config.emotiva.config.menuNotifyPort.label = Menu Port +thing-type.config.emotiva.config.menuNotifyPort.description = UDP port for menu endringer fra enheten +thing-type.config.emotiva.config.model.label = Enhetsmodell +thing-type.config.emotiva.config.model.description = Enhetsmodell for Emotiva enheten +thing-type.config.emotiva.config.revision.label = Enhetsversjon +thing-type.config.emotiva.config.revision.description = Revisjon av den angitte modeltypen +thing-type.config.emotiva.config.dataRevision.label = Datarevisjon +thing-type.config.emotiva.config.dataRevision.description = Revisjon av datatypen for angitt enhet +thing-type.config.emotiva.config.protocolVersion.label = Protokollversjon +thing-type.config.emotiva.config.protocolVersion.description = Versjon av Emotiva Network Remote Control +thing-type.config.emotiva.config.keepAlive.label = Keep Alive Notification +thing-type.config.emotiva.config.keepAlive.description = Tid, i milliseconds, mellom hver gang enheten sender en "keepAlive" melding + +# channel group types + +channel-group-type.emotiva.general.label = Generelle kontroller +channel-group-type.emotiva.general.description = Generelle kontroller for denne enheten +channel-group-type.emotiva.zone.label = Sonekontroll +channel-group-type.emotiva.zone.description = Kanaler for en angitt sone or denne enheten + +# channel types + +channel-type.emotiva.audio_input.label = Lydkilde +channel-type.emotiva.audio_input.description = Kilde for lyd +channel-type.emotiva.audio_bitstream.label = Lydbitstrøm +channel-type.emotiva.audio_bitstream.description = Nåværende lydbitstrøm, "PCM 2.0", "ATMOS", etc. +channel-type.emotiva.audio_bits.label = Audio Input Bits +channel-type.emotiva.audio_bits.description = Bits for nåværende lydstrøm: "32kHZ 24bits", etc. +channel-type.emotiva.bar.label = Frontpanel +channel-type.emotiva.bar.description = Viser text fra Frontpanel på enheten +channel-type.emotiva.channel.label = Radiokanal +channel-type.emotiva.channel.description = Bytte radiokanal opp eller ned +channel-type.emotiva.frequency.label = Radiofrekvens +channel-type.emotiva.frequency.description = Bytter radiofrekvens opp eller ned +channel-type.emotiva.dim.label = Lysstyrke Frontpanel +channel-type.emotiva.dim.description = Stegvis endring av lysstyrke på frontpanel, "0", "20", "40", "60", "80", "100" +channel-type.emotiva.input_name.label = Kildenavn +channel-type.emotiva.input_name.description = Navn på kilde eller modus +channel-type.emotiva.loudness.label = Loudness +channel-type.emotiva.loudness.description = Loudness PÅ/AV +channel-type.emotiva.mainPower.label = Strømforsyning +channel-type.emotiva.mainPower.description = Skruv enheten AV/PÅ +channel-type.emotiva.menu.label = Meny +channel-type.emotiva.menu.description = Kontroller for meny +channel-type.emotiva.mode.label = Modus +channel-type.emotiva.mode.description = Setter lydmodus, "Stereo", "Direct", "Auto", osv. +channel-type.emotiva.mode_surround.label = Surround-Modus +channel-type.emotiva.mode_surround.description = Setter surround modus, "Auto", "Stereo", "Dolby" osv. +channel-type.emotiva.mute.label = Demp +channel-type.emotiva.mute.description = Setter demp PÅ/AV for denne sonen +channel-type.emotiva.seek.label = Radio Kanalsøk +channel-type.emotiva.seek.description = Søker etter radiokanel, opp eller ned +channel-type.emotiva.selected_mode.label = Brukervalgt Modus +channel-type.emotiva.selected_mode.description = Brukervalgt modus for hovedsonene, der en satt verdi kan bli overstyrt av andre instillinger +channel-type.emotiva.selected_movie_music.label = Brukervalgt Film Musikk +channel-type.emotiva.selected_movie_music.description = Brukervalgt film eller musikk for hovesonene +channel-type.emotiva.speaker_preset.label = Høytaler Forhåndsinnstilling +channel-type.emotiva.speaker_preset.description = Valgt forhåndsinnstilling for høytalere +channel-type.emotiva.speaker_preset.state.option.preset1 = Høytalerkonfigurasjon 1 +channel-type.emotiva.speaker_preset.state.option.preset2 = Høytalerkonfigurasjon 2 +channel-type.emotiva.source.label = Kilde +channel-type.emotiva.source.description = Velge kilde for denne sonene på enheten +channel-type.emotiva.standby.label = Standby +channel-type.emotiva.standby.description = Sett enheten i standby modus +channel-type.emotiva.tuner_band.label = Radioband +channel-type.emotiva.tuner_band.description = Sett AM eller FM band +channel-type.emotiva.tuner_band.state.option.band_am = AM frekvensbånd +channel-type.emotiva.tuner_band.state.option.band_fm = FM frekvensbånd +channel-type.emotiva.tuner_channel.label = Radiokanalfrekvens +channel-type.emotiva.tuner_channel.description = Frekvens for valgte radiokanal +channel-type.emotiva.tuner_channel_select.label = Radiokanal +channel-type.emotiva.tuner_channel_select.description = Navn for valgte radiokanel +channel-type.emotiva.tuner_channel_select.option.channel_1 = Kanal 1 +channel-type.emotiva.tuner_channel_select.option.channel_2 = Kanal 2 +channel-type.emotiva.tuner_channel_select.option.channel_3 = Kanal 3 +channel-type.emotiva.tuner_channel_select.option.channel_4 = Kanal 4 +channel-type.emotiva.tuner_channel_select.option.channel_5 = Kanal 5 +channel-type.emotiva.tuner_channel_select.option.channel_6 = Kanal 6 +channel-type.emotiva.tuner_channel_select.option.channel_7 = Kanal 7 +channel-type.emotiva.tuner_channel_select.option.channel_8 = Kanal 8 +channel-type.emotiva.tuner_channel_select.option.channel_9 = Kanal 9 +channel-type.emotiva.tuner_channel_select.option.channel_10 = Kanal 10 +channel-type.emotiva.tuner_channel_select.option.channel_11 = Kanal 11 +channel-type.emotiva.tuner_channel_select.option.channel_12 = Kanal 12 +channel-type.emotiva.tuner_channel_select.option.channel_13 = Kanal 13 +channel-type.emotiva.tuner_channel_select.option.channel_14 = Kanal 14 +channel-type.emotiva.tuner_channel_select.option.channel_15 = Kanal 15 +channel-type.emotiva.tuner_channel_select.option.channel_16 = Kanal 16 +channel-type.emotiva.tuner_channel_select.option.channel_17 = Kanal 17 +channel-type.emotiva.tuner_channel_select.option.channel_18 = Kanal 18 +channel-type.emotiva.tuner_channel_select.option.channel_19 = Kanal 19 +channel-type.emotiva.tuner_channel_select.option.channel_20 = Kanal 20 +channel-type.emotiva.tuner_program.label = Radiosjanger +channel-type.emotiva.tuner_program.description = Sjanger for radioprogram: "Country", "Rock", "Klassisk", osv. +channel-type.emotiva.tuner_rds.label = Radio RDS +channel-type.emotiva.tuner_rds.description = Melding på Radio Data System (RDS) for valgte kanal +channel-type.emotiva.tuner_signal.label = Radio Signalstyrke +channel-type.emotiva.tuner_signal.description = Kvalitet på signalstyrket for valgt radio kanal +channel-type.emotiva.video_format.label = Videoformat +channel-type.emotiva.video_format.description = Videoformat for valgt videokilde: "1920x1080i/60", "3840x2160p/60", osv. +channel-type.emotiva.video_input.label = Videokilde +channel-type.emotiva.video_input.description = Kilde for video +channel-type.emotiva.video_space.label = Video Fargerom +channel-type.emotiva.video_space.description = Fargerom for videokilden: "YcbCr 8bits", osv. +channel-type.emotiva.volume.label = Lydnivå +channel-type.emotiva.volume.description = Sett lydnivået for denne sonen +channel-type.emotiva.volume_db.label = Lydnivå (dB) +channel-type.emotiva.volume_db.description = Sett lydnivået for denne sonen (dB). +channel-type.emotiva.volume_speaker_db.label = Høyttalertrim +channel-type.emotiva.volume_speaker_db.description = Øke/redusere lydnivå for høytaler, diskant eller bass, i +/-dB +channel-type.emotiva.zonePower.label = Strømforsyning (sone) +channel-type.emotiva.zonePower.description = Strøm AV/PÅ for denne sonen av enheten + + +# User Messages +message.processor.connecting = Kobler til +message.processor.connection.failed = Kunne ikke koble til, sjekk nettverksforbindelsen og konfigurasjon +message.processor.connection.error = Har ikke mottatt jevnlig melding fra enheten, sjekk nettverksforbindelsen +message.processor.notfound = Kunne ikke finne enheten med ipAddress {0} +message.processor.goodbye = Enheten ble skrudd av 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 0000000000000..2ea56ae84c279 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,433 @@ + + + + + + Emotiva Processor Thing for Emotiva Binding + + + + + + Channels for the main zone of this processor. + + + + Channels for zone2 of this processor. + + + + 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/Disable Mute on this zone of the Processor + + + + 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:Percentage + + 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 + + + + Number + + Radio tuner frequency + + + + Rollershutter + + Radio Tuner seek + + + + Rollershutter + + Radio Tuner Channel + + + + String + + Radio tuner band: "AM" or "FM" + + + + + + + + + + Number + + 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 + + Audio input source + + + + + String + + Audio input bitstream type: "PCM 2.0", "ATMOS", etc. + + + + + String + + Audio input bits: "32kHZ 24bits", etc. + + + + + String + + Video input source + + + + + String + + Video input format: "1920x1080i/60", "3840x2160p/60", etc. + + + + + String + + Video input space: "YcbCr 8bits", etc. + + + + + String + + User assigned 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. + + + + Number:Time + + Timestamp of last received keepAlive message. + + + + 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 0000000000000..95b10def36b92 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/AbstractDTOTestBase.java @@ -0,0 +1,295 @@ +/** + * 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 emotivaAck_PowerOff = """ + + + + """; + + protected String emotivaAck_PowerOffAndNotRealCommand = """ + + + + + """; + + protected String emotivaAck_PowerOffAndVolume = """ + + + + + """; + + protected String emotivaCommando_PowerOn = """ + """; + protected String emotivaNotify_emotivaProperty_Power = """ + """; + protected String emotivaUpdate_emotivaProperty_Power = """ + """; + protected String emotivaControl_Volume = """ + + + """; + + protected String emotivaNotifyV2_KeepAlive = """ + + + + """; + protected String emotivaNotifyV2_unknownTag = """ + + + + """; + protected String emotivaNotifyV2_KeepAlive_Sequence = "54062"; + + protected String emotivaNotifyV3_KeepAlive = """ + + + + """; + protected String emotivaUpdate_Request = """ + + + + + + + + + + + """; + + protected String emotivaMenuNotify = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; + protected String emotivaMenuNotify_WithCheckBox = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; + + protected String emotivaMenuNotify_Progress = """ + + + + """; + + protected String emotivaUpdate_Response_V2 = """ + + + + + + """; + + protected String emotivaUpdate_Response_V3 = """ + + + + + + """; + + protected String emotivaBarNotify_bigText = """ + + + + """; + + 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/EmotivaCommandHandlerTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaCommandHandlerTest.java new file mode 100644 index 0000000000000..243e2b4ee92ae --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaCommandHandlerTest.java @@ -0,0 +1,128 @@ +/** + * 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.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +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.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.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.PercentType; + +/** + * Unit tests for the EmotivaCommandHandler. + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +class EmotivaCommandHandlerTest { + + @Test + void volumeToPercentage() { + assertThat(EmotivaCommandHandler.volumeDecibelToPercentage("-100 dB")).isEqualTo(PercentType.valueOf("0")); + assertThat(EmotivaCommandHandler.volumeDecibelToPercentage(" -96")).isEqualTo(PercentType.valueOf("0")); + assertThat(EmotivaCommandHandler.volumeDecibelToPercentage("-41 dB ")).isEqualTo(PercentType.valueOf("50")); + assertThat(EmotivaCommandHandler.volumeDecibelToPercentage("15")).isEqualTo(PercentType.valueOf("100")); + assertThat(EmotivaCommandHandler.volumeDecibelToPercentage("20")).isEqualTo(PercentType.valueOf("100")); + } + + @Test + void volumeToDecibel() { + assertThat(EmotivaCommandHandler.volumePercentageToDecibel("-10")).isEqualTo(-96); + assertThat(EmotivaCommandHandler.volumePercentageToDecibel("0%")).isEqualTo(-96); + assertThat(EmotivaCommandHandler.volumePercentageToDecibel("50 %")).isEqualTo(-41); + assertThat(EmotivaCommandHandler.volumePercentageToDecibel("100 % ")).isEqualTo(15); + assertThat(EmotivaCommandHandler.volumePercentageToDecibel("110")).isEqualTo(15); + } + + @Test + public void testEmotivaVolumeValue() throws UnsupportedCommandTypeException { + + EmotivaConfiguration configuration = new EmotivaConfiguration(); + EmotivaCommandHandler commandHandler = new EmotivaCommandHandler(configuration); + + assertThat(commandHandler.emotivaVolumeValue(IncreaseDecreaseType.INCREASE)).isEqualTo(1); + assertThat(commandHandler.emotivaVolumeValue(IncreaseDecreaseType.DECREASE)).isEqualTo(-1); + assertThat(commandHandler.emotivaVolumeValue(new PercentType(0))).isEqualTo(-96); + assertThat(commandHandler.emotivaVolumeValue(new PercentType(50))).isEqualTo(-41); + assertThat(commandHandler.emotivaVolumeValue(new PercentType(100))).isEqualTo(15); + assertThat(commandHandler.emotivaVolumeValue(new DecimalType(1))).isEqualTo(1); + assertThat(commandHandler.emotivaVolumeValue(new DecimalType(100))).isEqualTo(15); + assertThat(commandHandler.emotivaVolumeValue(new DecimalType(-100))).isEqualTo(-96); + assertThat(commandHandler.emotivaVolumeValue(new DecimalType(-96))).isEqualTo(-96); + assertThat(commandHandler.emotivaVolumeValue(new DecimalType(-50))).isEqualTo(-50); + assertThatThrownBy(() -> commandHandler.emotivaVolumeValue(new DateTimeType("2020-04-01T20:04:30.395Z"))) + .isInstanceOf(UnsupportedCommandTypeException.class); + } + + private static Stream channelToControlRequest() { + return Stream.of( + Arguments.of(CHANNEL_SURROUND, "surround", DIMENSIONLESS_DECIBEL, surround, surround, surround, + surround_trim_set, PROTOCOL_V2, -24, 24), + Arguments.of(CHANNEL_SURROUND, "surround", DIMENSIONLESS_DECIBEL, surround, surround, surround, + surround_trim_set, PROTOCOL_V3, -24, 24), + 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, int min, int max) { + + final Map> commandMaps = new ConcurrentHashMap<>(); + + EmotivaControlRequest surround = EmotivaCommandHandler.channelToControlRequest(channel, commandMaps, version); + assertThat(surround.getName()).isEqualTo(name); + assertThat(surround.getChannel()).isEqualTo(channel); + assertThat(surround.getDataType()).isEqualTo(emotivaDataType); + assertThat(surround.getDefaultCommand()).isEqualTo(defaultCommand); + assertThat(surround.getOnCommand()).isEqualTo(onCommand); + assertThat(surround.getOffCommand()).isEqualTo(offCommand); + assertThat(surround.getSetCommand()).isEqualTo(setCommand); + assertThat(surround.getMinValue()).isEqualTo(min); + assertThat(surround.getMaxValue()).isEqualTo(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 0000000000000..6493dc45b3f29 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaAckDTOTest.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.assertj.core.api.Assertions.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(emotivaAck_PowerOff); + assertThat(dto).isNotNull(); + assertThat(dto.getCommands().size()).isEqualTo(1); + } + + @Test + void unmarshallOneValidCommand() throws JAXBException { + EmotivaAckDTO dto = (EmotivaAckDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaAck_PowerOffAndNotRealCommand); + assertThat(dto).isNotNull(); + List commands = xmlUtils.unmarshallXmlObjectsToControlCommands(dto.getCommands()); + assertThat(commands.size()).isEqualTo(2); + + assertThat(commands.get(0)).isNotNull(); + assertThat(commands.get(0).getName()).isEqualTo(EmotivaControlCommands.power_off.name()); + assertThat(commands.get(0).getStatus()).isEqualTo("ack"); + assertThat(commands.get(0).getVisible()).isNull(); + assertThat(commands.get(0).getValue()).isNull(); + + assertThat(commands.get(1)).isNotNull(); + assertThat(commands.get(1).getName()).isEqualTo(EmotivaControlCommands.none.name()); + assertThat(commands.get(1).getStatus()).isEqualTo("ack"); + assertThat(commands.get(1).getVisible()).isNull(); + assertThat(commands.get(1).getValue()).isNull(); + } +} 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 0000000000000..bbaecd74b5f8f --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaBarNotifyDTOTest.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 static org.assertj.core.api.Assertions.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(emotivaBarNotify_bigText); + assertThat(dto.getSequence()).isEqualTo("98"); + assertThat(dto.getTags().size()).isEqualTo(1); + + List commands = xmlUtils.unmarshallToBarNotify(dto.getTags()); + assertThat(commands.get(0).getType()).isEqualTo("bigText"); + assertThat(commands.get(0).getText()).isEqualTo("XBox One"); + assertThat(commands.get(0).getUnits()).isEqualTo(null); + assertThat(commands.get(0).getMin()).isEqualTo(null); + assertThat(commands.get(0).getMax()).isEqualTo(null); + } +} 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 0000000000000..68c746ed716c0 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaCommandDTOTest.java @@ -0,0 +1,74 @@ +/** + * 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.assertj.core.api.Assertions.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(emotivaCommando_PowerOn); + assertThat(commandDTO).isNotNull(); + assertThat(commandDTO.size()).isEqualTo(1); + assertThat(commandDTO.get(0).getName()).isEqualTo(EmotivaControlCommands.power_on.name()); + } + + @Test + void unmarshallFromEmotivaAckWithMissingEnumType() { + List commandDTO = xmlUtils.unmarshallToCommands(emotivaAck_PowerOffAndNotRealCommand); + assertThat(commandDTO).isNotNull(); + assertThat(commandDTO.size()).isEqualTo(2); + assertThat(commandDTO.get(0).getName()).isEqualTo(EmotivaControlCommands.power_off.name()); + assertThat(commandDTO.get(0).getStatus()).isEqualTo("ack"); + assertThat(commandDTO.get(0).getValue()).isNull(); + assertThat(commandDTO.get(0).getVisible()).isNull(); + assertThat(commandDTO.get(1).getName()).isEqualTo(EmotivaControlCommands.none.name()); + assertThat(commandDTO.get(1).getStatus()).isEqualTo("ack"); + assertThat(commandDTO.get(1).getValue()).isNull(); + assertThat(commandDTO.get(1).getVisible()).isNull(); + } + + @Test + void unmarshallFromEmotivaAck() { + List commandDTO = xmlUtils.unmarshallToCommands(emotivaAck_PowerOffAndVolume); + assertThat(commandDTO).isNotNull(); + assertThat(commandDTO.size()).isEqualTo(2); + assertThat(commandDTO.get(0).getName()).isEqualTo(EmotivaControlCommands.power_off.name()); + assertThat(commandDTO.get(0).getStatus()).isEqualTo("ack"); + assertThat(commandDTO.get(0).getValue()).isNull(); + assertThat(commandDTO.get(0).getVisible()).isNull(); + assertThat(commandDTO.get(1).getName()).isEqualTo(EmotivaControlCommands.volume.name()); + assertThat(commandDTO.get(1).getStatus()).isEqualTo("ack"); + assertThat(commandDTO.get(1).getValue()).isNull(); + assertThat(commandDTO.get(1).getVisible()).isNull(); + } +} 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 0000000000000..64fdf0423b7a7 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaControlDTOTest.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.assertj.core.api.Assertions.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).contains(""); + assertThat(xmlString).doesNotContain(""); + } + + @Test + void marshalNoCommand() { + EmotivaControlDTO control = new EmotivaControlDTO(Collections.emptyList()); + String xmlString = xmlUtils.marshallJAXBElementObjects(control); + assertThat(xmlString).contains(""); + } + + @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).contains(""); + assertThat(xmlString).contains(""); + 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).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + 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 0000000000000..793a879e98dee --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaMenuNotifyDTOTest.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.assertj.core.api.Assertions.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()).isEqualTo(null); + assertThat(dto.getSequence()).isEqualTo("2378"); + assertThat(dto.getRow().size()).isEqualTo(11); + assertThat(dto.getRow().size()).isEqualTo(11); + assertThat(dto.getRow().get(0).getNumber()).isEqualTo("0"); + assertThat(dto.getRow().get(0).getCol().size()).isEqualTo(3); + assertThat(dto.getRow().get(0).getCol().get(0).getNumber()).isEqualTo("0"); + assertThat(dto.getRow().get(0).getCol().get(0).getValue()).isEqualTo(""); + assertThat(dto.getRow().get(0).getCol().get(0).getHighlight()).isEqualTo("no"); + assertThat(dto.getRow().get(0).getCol().get(0).getArrow()).isEqualTo("no"); + assertThat(dto.getRow().get(0).getCol().get(1).getNumber()).isEqualTo("1"); + assertThat(dto.getRow().get(0).getCol().get(1).getValue()).isEqualTo("Left Display"); + assertThat(dto.getRow().get(0).getCol().get(1).getHighlight()).isEqualTo("no"); + assertThat(dto.getRow().get(0).getCol().get(1).getArrow()).isEqualTo("up"); + assertThat(dto.getRow().get(0).getCol().get(2).getNumber()).isEqualTo("2"); + assertThat(dto.getRow().get(0).getCol().get(2).getValue()).isEqualTo("Full Status"); + assertThat(dto.getRow().get(0).getCol().get(2).getHighlight()).isEqualTo("no"); + assertThat(dto.getRow().get(0).getCol().get(2).getArrow()).isEqualTo("no"); + } + + @Test + void testUnmarshallProgress() throws JAXBException { + EmotivaMenuNotifyDTO dto = (EmotivaMenuNotifyDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaMenuNotify_Progress); + assertThat(dto.getSequence()).isEqualTo("2405"); + assertThat(dto.getRow()).isEqualTo(null); + assertThat(dto.getProgress().getTime()).isEqualTo("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 0000000000000..c08185a1e75b3 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaNotifyWrapperTest.java @@ -0,0 +1,93 @@ +/** + * 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.assertj.core.api.Assertions.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(emotivaNotifyV2_KeepAlive_Sequence, + Collections.emptyList()); + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString).contains(""); + assertThat(xmlAsString).doesNotContain(""); + } + + @Test + void marshallWithOneProperty() { + List keepAliveProperty = List.of(new EmotivaPropertyDTO("keepAlive", "7500", "true")); + EmotivaNotifyWrapper dto = new EmotivaNotifyWrapper(emotivaNotifyV2_KeepAlive_Sequence, keepAliveProperty); + + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString).contains(""); + assertThat(xmlAsString).contains(""); + assertThat(xmlAsString).contains(""); + } + + @Test + void testUnmarshallV2() throws JAXBException { + EmotivaNotifyWrapper dto = (EmotivaNotifyWrapper) xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2_KeepAlive); + assertThat(dto.getSequence()).isEqualTo(emotivaNotifyV2_KeepAlive_Sequence); + assertThat(dto.getTags().size()).isEqualTo(1); + assertThat(dto.getTags().get(0)).isInstanceOf(Element.class); + Element keepAlive = (Element) dto.getTags().get(0); + assertThat(keepAlive.getTagName()).isEqualTo(EmotivaSubscriptionTags.keepAlive.name()); + assertThat(keepAlive.hasAttribute("value")).isTrue(); + assertThat(keepAlive.getAttribute("value")).isEqualTo("7500"); + assertThat(keepAlive.hasAttribute("visible")).isTrue(); + assertThat(keepAlive.getAttribute("visible")).isEqualTo("true"); + assertThat(dto.getProperties()).isNull(); + } + + @Test + void testUnmarshallV2UnknownProperty() throws JAXBException { + EmotivaNotifyWrapper dto = (EmotivaNotifyWrapper) xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2_unknownTag); + assertThat(dto.getSequence()).isEqualTo(emotivaNotifyV2_KeepAlive_Sequence); + assertThat(dto.getTags().size()).isEqualTo(1); + assertThat(dto.getTags().get(0)).isInstanceOf(Element.class); + Element unknownCommand = (Element) dto.getTags().get(0); + assertThat(unknownCommand.getTagName()).isEqualTo("unknownTag"); + assertThat(dto.getProperties()).isNull(); + } + + @Test + void testUnmarshallV3() throws JAXBException { + EmotivaNotifyWrapper dto = (EmotivaNotifyWrapper) xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV3_KeepAlive); + assertThat(dto.getSequence()).isEqualTo(emotivaNotifyV2_KeepAlive_Sequence); + assertThat(dto.getProperties().size()).isEqualTo(1); + assertThat(dto.getTags()).isNull(); + } +} 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 0000000000000..e48c3b219f380 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPingDTOTest.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.assertj.core.api.Assertions.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).contains(""); + assertThat(xmlAsString).doesNotContain(""); + } + + @Test + void marshallWithProtocol() { + EmotivaPingDTO dto = new EmotivaPingDTO("3.0"); + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString).contains(""); + assertThat(xmlAsString).doesNotContain(""); + } + + @Test + void unmarshallV2() throws JAXBException { + EmotivaPingDTO dto = (EmotivaPingDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaPingV2); + assertThat(dto).isNotNull(); + assertThat(dto.getProtocol()).isNull(); + } + + @Test + void unmarshallV3() throws JAXBException { + EmotivaPingDTO dto = (EmotivaPingDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaPingV3); + assertThat(dto).isNotNull(); + assertThat(dto.getProtocol()).isNotNull(); + assertThat(dto.getProtocol()).isEqualTo("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 0000000000000..30a851de3eca8 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaPropertyDTOTest.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.assertj.core.api.Assertions.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(emotivaNotify_emotivaProperty_Power); + assertThat(commandDTO).isNotNull(); + assertThat(commandDTO.getName()).isEqualTo(EmotivaSubscriptionTags.tuner_channel.name()); + assertThat(commandDTO.getValue()).isEqualTo("FM 106.50MHz"); + assertThat(commandDTO.getVisible()).isEqualTo("true"); + assertThat(commandDTO.getStatus()).isNull(); + } + + @Test + void unmarshallFromEmotivaUpdate() throws JAXBException { + EmotivaPropertyDTO commandDTO = (EmotivaPropertyDTO) xmlUtils + .unmarshallToEmotivaDTO(emotivaUpdate_emotivaProperty_Power); + assertThat(commandDTO).isNotNull(); + assertThat(commandDTO.getName()).isEqualTo(EmotivaControlCommands.power.name()); + assertThat(commandDTO.getValue()).isEqualTo("On"); + assertThat(commandDTO.getVisible()).isEqualTo("true"); + assertThat(commandDTO.getStatus()).isEqualTo(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 0000000000000..49cbc1255dda7 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequestTest.java @@ -0,0 +1,91 @@ +/** + * 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.assertj.core.api.Assertions.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).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + } + + @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).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).doesNotContain(""); + assertThat(xmlString).doesNotContain(""); + } + + @Test + void unmarshall() throws JAXBException { + var dto = (EmotivaSubscriptionResponse) xmlUtils.unmarshallToEmotivaDTO(emotivaSubscriptionRequest); + assertThat(dto).isNotNull(); + assertThat(dto.getTags().size()).isEqualTo(3); + assertThat(dto.getProperties()).isNull(); + + List commands = xmlUtils.unmarshallToNotification(dto.getTags()); + + assertThat(commands.get(0).getName()).isEqualTo(EmotivaSubscriptionTags.selected_mode.name()); + assertThat(commands.get(0).getStatus()).isNull(); + assertThat(commands.get(0).getValue()).isNull(); + assertThat(commands.get(0).getVisible()).isNull(); + + assertThat(commands.get(1).getName()).isEqualTo(EmotivaSubscriptionTags.power.name()); + assertThat(commands.get(1).getStatus()).isNull(); + assertThat(commands.get(1).getValue()).isNull(); + assertThat(commands.get(1).getVisible()).isNull(); + + assertThat(commands.get(2).getName()).isEqualTo("unknown"); + assertThat(commands.get(2).getStatus()).isNull(); + assertThat(commands.get(2).getValue()).isNull(); + assertThat(commands.get(2).getVisible()).isNull(); + } +} 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 0000000000000..de54ec09e71f1 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionResponseTest.java @@ -0,0 +1,99 @@ +/** + * 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.assertj.core.api.Assertions.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).contains(""); + assertThat(xmlString).doesNotContain(""); + assertThat(xmlString).doesNotContain(""); + assertThat(xmlString).doesNotContain(""); + } + + @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).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).doesNotContain(""); + assertThat(xmlString).doesNotContain(""); + assertThat(xmlString).contains(""); + } + + @Test + void unmarshall() throws JAXBException { + var dto = (EmotivaSubscriptionResponse) xmlUtils.unmarshallToEmotivaDTO(emotivaSubscriptionResponse); + assertThat(dto.tags).isNotNull(); + assertThat(dto.tags.size()).isEqualTo(5); + List commands = xmlUtils.unmarshallToNotification(dto.getTags()); + assertThat(commands).isNotNull(); + assertThat(commands.size()).isEqualTo(dto.tags.size()); + assertThat(commands.get(0)).isInstanceOf(EmotivaNotifyDTO.class); + assertThat(commands.get(0).getName()).isEqualTo(EmotivaSubscriptionTags.power.name()); + assertThat(commands.get(0).getStatus()).isEqualTo(EmotivaPropertyStatus.VALID.getValue()); + assertThat(commands.get(0).getVisible()).isNull(); + assertThat(commands.get(0).getValue()).isNull(); + + assertThat(commands.get(1).getName()).isEqualTo(EmotivaSubscriptionTags.source.name()); + assertThat(commands.get(1).getValue()).isEqualTo("SHIELD "); + assertThat(commands.get(1).getVisible()).isEqualTo("true"); + assertThat(commands.get(1).getStatus()).isEqualTo(EmotivaPropertyStatus.VALID.getValue()); + + assertThat(commands.get(2).getName()).isEqualTo(EmotivaSubscriptionTags.menu.name()); + assertThat(commands.get(2).getValue()).isEqualTo("Off"); + assertThat(commands.get(2).getVisible()).isEqualTo("true"); + assertThat(commands.get(2).getStatus()).isEqualTo(EmotivaPropertyStatus.VALID.getValue()); + + assertThat(commands.get(3).getName()).isEqualTo(EmotivaSubscriptionTags.treble.name()); + assertThat(commands.get(3).getValue()).isEqualTo("+ 1.5"); + assertThat(commands.get(3).getVisible()).isEqualTo("true"); + assertThat(commands.get(3).getStatus()).isEqualTo(EmotivaPropertyStatus.VALID.getValue()); + assertThat(commands.get(3).getAck()).isEqualTo("yes"); + + assertThat(commands.get(4).getName()).isEqualTo(EmotivaSubscriptionTags.UNKNOWN_TAG); + assertThat(commands.get(4).getValue()).isNull(); + assertThat(commands.get(4).getVisible()).isNull(); + assertThat(commands.get(4).getStatus()).isNull(); + assertThat(commands.get(4).getAck()).isEqualTo("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 0000000000000..d5b5e3dee6325 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaTransponderDTOTest.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.assertj.core.api.Assertions.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).isNotNull(); + assertThat(dto.getModel()).isEqualTo("XMC-1"); + assertThat(dto.getRevision()).isEqualTo("2.0"); + assertThat(dto.getName()).isEqualTo("Living Room"); + assertThat(dto.getControl().getVersion()).isEqualTo("2.0"); + assertThat(dto.getControl().getControlPort()).isEqualTo(7002); + assertThat(dto.getControl().getNotifyPort()).isEqualTo(7003); + assertThat(dto.getControl().getInfoPort()).isEqualTo(7004); + assertThat(dto.getControl().getSetupPortTCP()).isEqualTo(7100); + assertThat(dto.getControl().getKeepAlive()).isEqualTo(10000); + } + + @Test + void unmarshallV3() throws JAXBException { + EmotivaTransponderDTO dto = (EmotivaTransponderDTO) xmlUtils + .unmarshallToEmotivaDTO(emotivaTransponderResponseV3); + assertThat(dto).isNotNull(); + assertThat(dto.getModel()).isEqualTo("XMC-2"); + assertThat(dto.getRevision()).isEqualTo("3.0"); + assertThat(dto.getName()).isEqualTo("Living Room"); + assertThat(dto.getControl().getVersion()).isEqualTo("3.0"); + assertThat(dto.getControl().getControlPort()).isEqualTo(7002); + assertThat(dto.getControl().getNotifyPort()).isEqualTo(7003); + assertThat(dto.getControl().getInfoPort()).isEqualTo(7004); + assertThat(dto.getControl().getSetupPortTCP()).isEqualTo(7100); + assertThat(dto.getControl().getKeepAlive()).isEqualTo(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 0000000000000..aa948de33a245 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscriptionTest.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.assertj.core.api.Assertions.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).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + } + + @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).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).doesNotContain(""); + assertThat(xmlString).doesNotContain(""); + } +} 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 0000000000000..3e6c99498f2da --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateRequestTest.java @@ -0,0 +1,55 @@ +/** + * 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.assertj.core.api.Assertions.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.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).contains(""); + assertThat(xmlAsString).doesNotContain(""); + } + + @Test + void marshalFromChannelUID() { + EmotivaSubscriptionTags subscriptionChannel = EmotivaSubscriptionTags.fromChannelUID("general#tuner_RDS"); + EmotivaUpdateRequest emotivaUpdateRequest = new EmotivaUpdateRequest(subscriptionChannel); + String xmlString = xmlUtils.marshallJAXBElementObjects(emotivaUpdateRequest); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + assertThat(xmlString).contains(""); + } +} 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 0000000000000..72c555f00ac61 --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUpdateResponseTest.java @@ -0,0 +1,96 @@ +/** + * 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.assertj.core.api.Assertions.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).contains(""); + assertThat(xmlAsString).doesNotContain(""); + } + + @Test + void unmarshallV2() throws JAXBException { + var dto = (EmotivaUpdateResponse) xmlUtils.unmarshallToEmotivaDTO(emotivaUpdate_Response_V2); + assertThat(dto).isNotNull(); + assertThat(dto.getProperties()).isNull(); + List notifications = xmlUtils.unmarshallToNotification(dto.getTags()); + assertThat(notifications.size()).isEqualTo(3); + + assertThat(notifications.get(0).getName()).isEqualTo(EmotivaSubscriptionTags.power.name()); + assertThat(notifications.get(0).getValue()).isEqualTo("On"); + assertThat(notifications.get(0).getVisible()).isEqualTo("true"); + assertThat(notifications.get(0).getStatus()).isEqualTo(VALID.getValue()); + + assertThat(notifications.get(1).getName()).isEqualTo(EmotivaSubscriptionTags.source.name()); + assertThat(notifications.get(1).getValue()).isEqualTo("HDMI 1"); + assertThat(notifications.get(1).getVisible()).isEqualTo("true"); + assertThat(notifications.get(1).getStatus()).isEqualTo(NOT_VALID.getValue()); + + assertThat(notifications.get(2).getName()).isEqualTo(EmotivaSubscriptionTags.unknown.name()); + assertThat(notifications.get(2).getStatus()).isNull(); + assertThat(notifications.get(2).getValue()).isNull(); + assertThat(notifications.get(2).getVisible()).isNull(); + } + + @Test + void unmarshallV3() throws JAXBException { + var dto = (EmotivaUpdateResponse) xmlUtils.unmarshallToEmotivaDTO(emotivaUpdate_Response_V3); + assertThat(dto).isNotNull(); + assertThat(dto.getTags()).isNull(); + assertThat(dto.getProperties().size()).isEqualTo(3); + + assertThat(dto.getProperties().get(0)).isInstanceOf(EmotivaPropertyDTO.class); + assertThat(dto.getProperties().get(0).getName()).isEqualTo(EmotivaSubscriptionTags.power.name()); + assertThat(dto.getProperties().get(0).getValue()).isEqualTo("On"); + assertThat(dto.getProperties().get(0).getVisible()).isEqualTo("true"); + assertThat(dto.getProperties().get(0).getStatus()).isEqualTo(VALID.getValue()); + + assertThat(dto.getProperties().get(1).getName()).isEqualTo(EmotivaSubscriptionTags.source.name()); + assertThat(dto.getProperties().get(1).getValue()).isEqualTo("HDMI 1"); + assertThat(dto.getProperties().get(1).getVisible()).isEqualTo("true"); + assertThat(dto.getProperties().get(1).getStatus()).isEqualTo(NOT_VALID.getValue()); + + assertThat(dto.getProperties().get(2).getName()).isEqualTo("noKnownTag"); + assertThat(dto.getProperties().get(2).getStatus()).isNull(); + assertThat(dto.getProperties().get(2).getValue()).isNull(); + assertThat(dto.getProperties().get(2).getVisible()).isNull(); + } +} 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 0000000000000..04b6cfa37beed --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequestTest.java @@ -0,0 +1,370 @@ +/** + * 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.assertj.core.api.Assertions.assertThat; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_AUDIO_BITS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_AUDIO_BITSTREAM; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_AUDIO_INPUT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_BACK; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_BASS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_CENTER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_CHANNEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_DIM; +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_INPUT1; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT2; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT3; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT4; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT5; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT6; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT7; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_INPUT8; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_LOUDNESS; +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_MAIN_ZONE_POWER; +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; +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_SELECTED_MODE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SELECTED_MOVIE_MUSIC; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SOURCE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SPEAKER_PRESET; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_STANDBY; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SUBWOOFER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SURROUND; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TREBLE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_BAND; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_CHANNEL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_CHANNEL_SELECT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_PROGRAM; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_RDS; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_SIGNAL; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_VIDEO_FORMAT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_VIDEO_INPUT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_VIDEO_SPACE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_WIDTH; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_INPUT; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_MUTE; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_POWER; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME_DB; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_SUBSCRIPTION_PROPERTY_ACK; +import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.NAME_SOURCES_MAP; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.all_stereo; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.auto; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.back_trim_set; +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.bass_down; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.center_trim_set; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.channel; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.channel_1; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.channel_2; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.channel_3; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.coax1; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.dim; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.direct; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.dolby; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.down; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.dts; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.enter; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.frequency; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.hdmi1; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.height_trim_set; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.left; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.loudness_off; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.loudness_on; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.menu; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.mode_down; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.mode_up; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.movie; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.music; +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.none; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.power_off; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.power_on; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.preset2; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.reference_stereo; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.right; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.seek; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.set_volume; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.source_1; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.source_2; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.speaker_preset; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.standby; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.stereo; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.subwoofer_trim_set; +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.treble_down; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.treble_up; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.up; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.width_trim_set; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.zone2_input; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.zone2_mute_off; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.zone2_mute_on; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.zone2_power_off; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.zone2_power_on; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.zone2_set_volume; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V2; +import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V3; +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.EmotivaCommandHandler; +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("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, 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_INPUT, new DecimalType(1), zone2_input, PROTOCOL_V2, "1"), + Arguments.of(CHANNEL_ZONE2_INPUT, new DecimalType(-3), zone2_input, PROTOCOL_V2, "-1"), + 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 sourcesMap = new EnumMap<>( + EmotivaControlCommands.class); + private static final EnumMap channelMap = new EnumMap<>( + EmotivaControlCommands.class); + private static final EnumMap bandMap = new EnumMap<>(EmotivaControlCommands.class); + + private static final Map stateMap = Collections.synchronizedMap(new HashMap<>()); + private static final Map> commandMaps = new ConcurrentHashMap<>(); + + @BeforeAll + static void beforeAll() { + sourcesMap.put(source_1, "HDMI 1"); + sourcesMap.put(source_2, "SHIELD"); + sourcesMap.put(hdmi1, "HDMI1"); + sourcesMap.put(coax1, "Coax 1"); + channelMap.put(channel_1, "Channel 1"); + channelMap.put(channel_2, "Channel 2"); + channelMap.put(channel_3, "My Radio Channel"); + bandMap.put(band_am, "AM"); + bandMap.put(band_fm, "FM"); + commandMaps.put(NAME_SOURCES_MAP, sourcesMap); + commandMaps.put(tuner_channel.getName(), channelMap); + commandMaps.put(tuner_band.getName(), bandMap); + + stateMap.put(CHANNEL_TREBLE, new DecimalType(-3)); + stateMap.put(CHANNEL_TUNER_CHANNEL, new StringType("FM 87.50MHz")); + stateMap.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 = EmotivaCommandHandler.channelToControlRequest(channel, commandMaps, + protocolVersion); + + EmotivaControlDTO dto = controlRequest.createDTO(ohValue, stateMap.get(channel)); + assertThat(dto.getCommands().size()).isEqualTo(1); + assertThat(dto.getCommands().get(0).getName()).isEqualTo(controlCommand.name()); + assertThat(dto.getCommands().get(0).getValue()).isEqualTo(requestValue); + assertThat(dto.getCommands().get(0).getVisible()).isEqualTo(null); + assertThat(dto.getCommands().get(0).getStatus()).isEqualTo(null); + assertThat(dto.getCommands().get(0).getAck()).isEqualTo(DEFAULT_SUBSCRIPTION_PROPERTY_ACK); + } +} 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 0000000000000..750cc8ca7307d --- /dev/null +++ b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaXmlUtilsTest.java @@ -0,0 +1,77 @@ +/** + * 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.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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; +import org.xml.sax.SAXParseException; + +/** + * 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() { + assertThatThrownBy(() -> xmlUtils.unmarshallToEmotivaDTO("")).isInstanceOf(JAXBException.class) + .hasMessageContaining("xml value is null or empty"); + } + + @Test + void testUnmarshallNotValidXML() { + assertThatThrownBy(() -> xmlUtils.unmarshallToEmotivaDTO("notXmlAtAll")).isInstanceOf(UnmarshalException.class) + .hasCauseInstanceOf(SAXParseException.class); + } + + @Test + void testUnmarshallInstanceObject() throws JAXBException { + Object object = xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2_KeepAlive); + + assertThat(object).isInstanceOf(EmotivaNotifyWrapper.class); + } + + @Test + void testUnmarshallXml() throws JAXBException { + Object object = xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2_KeepAlive); + + assertThat(object).isInstanceOf(EmotivaNotifyWrapper.class); + } + + @Test + void testMarshallObjectWithoutXmlElements() { + String commands = xmlUtils.marshallEmotivaDTO(""); + assertThat(commands).isEmpty(); + } + + @Test + void testMarshallNoValueDTO() { + EmotivaNotifyWrapper dto = new EmotivaNotifyWrapper(); + String xmlAsString = xmlUtils.marshallEmotivaDTO(dto); + assertThat(xmlAsString).doesNotContain(""); + assertThat(xmlAsString).contains(""); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 0faf1fb3161a5..14b3a8a8e59b6 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -127,6 +127,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