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