[emotiva] Initial contribution (#16499)

* [emotiva] Initial contribution

Signed-off-by: Espen Fossen <espenaf@junta.no>
This commit is contained in:
Espen Fossen 2024-06-13 22:33:14 +02:00 committed by GitHub
parent 790d4d09ca
commit 67e6f8bf30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 7960 additions and 0 deletions

View File

@ -96,6 +96,7 @@
/bundles/org.openhab.binding.electroluxair/ @jannegpriv
/bundles/org.openhab.binding.elerotransmitterstick/ @vbier
/bundles/org.openhab.binding.elroconnects/ @mherwege
/bundles/org.openhab.binding.emotiva/ @espenaf
/bundles/org.openhab.binding.energenie/ @hmerk
/bundles/org.openhab.binding.energidataservice/ @jlaur
/bundles/org.openhab.binding.enigma2/ @gdolfen

View File

@ -471,6 +471,11 @@
<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>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,190 @@
# Emotiva Binding
This binding integrates Emotiva AV processors by using the Emotiva Network Remote Control protocol.
## Supported Things
This binding supports Emotiva processors with Emotiva Network Remote Control protocol support.
The thing type for all of them is `processor`.
Tested models: Emotiva XMC-2
## Discovery
The binding automatically discovers devices on your network.
## Thing Configuration
The Emotiva Processor thing requires the `ipAddress` it can connect to.
There are more parameters which all have defaults set.
| Parameter | Values | Default |
|-----------------------|---------------------------------------------------------------|---------|
| ipAddress | IP address of the processor | - |
| controlPort | port number, e.g. 7002 | 7002 |
| notifyPort | port number, e.g. 7003 | 7003 |
| infoPort | port number, e.g. 7004 | 7004 |
| setupPortTCP | port number, e.g. 7100 | 7100 |
| menuNotifyPort | port number, e.g. 7005 | 7005 |
| protocolVersion | Emotiva Network Protocol version, e.g. 3.0 | 2.0 |
| keepAlive | Time between notification update from device, in milliseconds | 7500 |
| retryConnectInMinutes | Time between connection retry, in minutes | 2 |
## Channels
The Emotiva Processor supports the following channels (some channels are model specific):
| Channel Type ID | Item Type | Description |
|------------------------------------|--------------------|------------------------------------------------------------|
| _Main zone_ | | |
| main-zone#power | Switch (RW) | Main zone power on/off |
| main-zone#volume | Dimmer (RW) | Main zone volume in percentage (0 to 100) |
| main-zone#volume-db | Number (RW) | Main zone volume in dB (-96 to 15) |
| main-zone#mute | Switch (RW) | Main zone mute |
| main-zone#source | String (RW) | Main zone input (HDMI1, TUNER, ARC, ...) |
| _Zone 2_ | | |
| zone2#power | Switch (RW) | Zone 2 power on/off |
| zone2#volume | Dimmer (RW) | Zone 2 volume in percentage (0 to 100) |
| zone2#volume-db | Number (RW) | Zone 2 volume in dB (-80 offset) |
| zone2#mute | Switch (RW) | Zone 2 mute |
| zone2#input | String (RW) | Zone 2 input |
| _General_ | | |
| general#power | Switch (RW) | Power on/off |
| general#standby | String (W) | Set in standby mode |
| general#menu | String (RW) | Enter or exit menu |
| general#menu-control | String (W) | Control menu via string commands |
| general#up | String (W) | Menu up |
| general#down | String (W) | Menu down |
| general#left | String (W) | Menu left |
| general#right | String (W) | Menu right |
| general#enter | String (W) | Menu enter |
| general#dim | Switch (RW) | Cycle through FP dimness settings |
| general#mode | String (RW) | Select audio mode (auto, dts, ...) |
| general#info | String (W) | Show info screen |
| general#speaker-preset | String (RW) | Select speaker presets (preset1, preset2) |
| general#center | Number (RW) | Center Volume increment up/down (0.5 step) |
| general#subwoofer | Number (RW) | Subwoofer Volume increment up/down (0.5 step) |
| general#surround | Number (RW) | Surround Volume increment up/down (0.5 step) |
| general#back | Number (RW) | Back Volume increment up/down (0.5 step) |
| general#loudness | Switch (RW) | Loudness on/off |
| general#treble | Number (RW) | Treble Volume increment up/down (0.5 step) |
| general#bass | Number (RW) | Bass Volume increment up/down (0.5 step) |
| general#frequenncy | Rollershutter (W) | Frequency up/down, (100 kHz step) |
| general#seek | Rollershutter (W) | Seek signal up/down |
| general#channel | Rollershutter (W) | Channel up/down |
| general#tuner-band | String (R) | Tuner band, (AM, FM) |
| general#tuner-channel | String (RW) | Userassigned 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)

View File

@ -0,0 +1,17 @@
<?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>

View File

@ -0,0 +1,9 @@
<?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>

View File

@ -0,0 +1,171 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link EmotivaBindingConstants} class defines common constants, which are used across the whole binding.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class EmotivaBindingConstants {
public static final String BINDING_ID = "emotiva";
/** Property name to uniquely identify (discovered) things. */
static final String UNIQUE_PROPERTY_NAME = "ip4Address";
/** Default port used to discover Emotiva devices. */
static final int DEFAULT_PING_PORT = 7000;
/** Default port used to receive transponder (discovered) Emotiva devices. */
static final int DEFAULT_TRANSPONDER_PORT = 7001;
/** Default timeout in milliseconds for sending UDP packets. */
static final int DEFAULT_UDP_SENDING_TIMEOUT = 1000;
/** Number of connection attempts, set OFFLINE if no success and a retry job is then started. */
static final int DEFAULT_CONNECTION_RETRIES = 3;
/** Connection retry interval in minutes */
static final int DEFAULT_RETRY_INTERVAL_MINUTES = 2;
/**
* Default Emotiva device keep alive in milliseconds. {@link org.openhab.binding.emotiva.internal.dto.ControlDTO}
*/
static final int DEFAULT_KEEP_ALIVE_IN_MILLISECONDS = 7500;
/** State name for storing keepAlive timestamp messages */
public static final String LAST_SEEN_STATE_NAME = "no-channel#last-seen";
/**
* Default Emotiva device considered list in milliseconds.
* {@link org.openhab.binding.emotiva.internal.dto.ControlDTO}
*/
static final int DEFAULT_KEEP_ALIVE_CONSIDERED_LOST_IN_MILLISECONDS = 30000;
/** Default Emotiva control message value **/
public static final String DEFAULT_CONTROL_MESSAGE_SET_DEFAULT_VALUE = "0";
/** Default value for ack property in Emotiva control messages **/
public static final String DEFAULT_CONTROL_ACK_VALUE = "yes";
/** Default discovery timeout in seconds **/
public static final int DISCOVERY_TIMEOUT_SECONDS = 5;
/** Default discovery broadcast address **/
public static final String DISCOVERY_BROADCAST_ADDRESS = "255.255.255.255";
/** List of all Thing Type UIDs **/
static final ThingTypeUID THING_PROCESSOR = new ThingTypeUID(BINDING_ID, "processor");
public static final Set<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";
}

View File

@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlRequest;
import org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
import org.openhab.binding.emotiva.internal.protocol.OHChannelToEmotivaCommand;
import org.openhab.core.library.types.PercentType;
/**
* Helper class for Emotiva commands.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class EmotivaCommandHelper {
public static PercentType volumeDecibelToPercentage(String volumeInDecibel) {
String volumeTrimmed = volumeInDecibel.replace("dB", "").trim();
int clampedValue = clamp(volumeTrimmed, DEFAULT_VOLUME_MIN_DECIBEL, DEFAULT_VOLUME_MAX_DECIBEL);
return new PercentType(Math.round((100 - ((float) Math.abs(clampedValue - DEFAULT_VOLUME_MAX_DECIBEL)
/ Math.abs(DEFAULT_VOLUME_MIN_DECIBEL - DEFAULT_VOLUME_MAX_DECIBEL)) * 100)));
}
public static double integerToPercentage(int integer) {
int clampedValue = clamp(integer, 0, 100);
return Math.round((100 - ((float) Math.abs(clampedValue - 100) / Math.abs(-100)) * 100));
}
public static int volumePercentageToDecibel(int volumeInPercentage) {
int clampedValue = clamp(volumeInPercentage, 0, 100);
return (clampedValue * (DEFAULT_VOLUME_MAX_DECIBEL - DEFAULT_VOLUME_MIN_DECIBEL) / 100)
+ DEFAULT_VOLUME_MIN_DECIBEL;
}
public static int volumePercentageToDecibel(String volumeInPercentage) {
String volumeInPercentageTrimmed = volumeInPercentage.replace("%", "").trim();
int clampedValue = clamp(volumeInPercentageTrimmed, 0, 100);
return (clampedValue * (DEFAULT_VOLUME_MAX_DECIBEL - DEFAULT_VOLUME_MIN_DECIBEL) / 100)
+ DEFAULT_VOLUME_MIN_DECIBEL;
}
public static double clamp(Number value, double min, double max) {
return Math.min(Math.max(value.intValue(), min), max);
}
private static int clamp(String volumeInPercentage, int min, int max) {
return Math.min(Math.max(Double.valueOf(volumeInPercentage.trim()).intValue(), min), max);
}
private static int clamp(int volumeInPercentage, int min, int max) {
return Math.min(Math.max(Double.valueOf(volumeInPercentage).intValue(), min), max);
}
public static EmotivaControlRequest channelToControlRequest(String id,
Map<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();
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link EmotivaConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class EmotivaConfiguration {
public String ipAddress = "";
public int controlPort = 7002;
public int notifyPort = 7003;
public int infoPort = 7004;
public int setupPortTCP = 7100;
public int menuNotifyPort = 7005;
public String protocolVersion = DEFAULT_EMOTIVA_PROTOCOL_VERSION;
public int keepAlive = DEFAULT_KEEP_ALIVE_IN_MILLISECONDS;
public int retryConnectInMinutes = DEFAULT_RETRY_INTERVAL_MINUTES;
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.THING_PROCESSOR;
import java.util.Set;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link org.openhab.core.thing.binding.ThingHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.emotiva", service = ThingHandlerFactory.class)
public class EmotivaHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(EmotivaHandlerFactory.class);
private static final Set<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;
}
}

View File

@ -0,0 +1,786 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import static java.lang.String.format;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.channelToControlRequest;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.getMenuPanelColumnLabel;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.getMenuPanelRowLabel;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.updateProgress;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumeDecibelToPercentage;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumePercentageToDecibel;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.band_am;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.band_fm;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.channel_1;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.none;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.power_on;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.STRING;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus.NOT_VALID;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.protocolFromConfig;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.noSubscriptionToChannel;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_band;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_channel;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.measure.quantity.Frequency;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.emotiva.internal.dto.AbstractNotificationDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaAckDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaBarNotifyDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaBarNotifyWrapper;
import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaMenuNotifyDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyWrapper;
import org.openhab.binding.emotiva.internal.dto.EmotivaPropertyDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionResponse;
import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateResponse;
import org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlRequest;
import org.openhab.binding.emotiva.internal.protocol.EmotivaDataType;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
import org.openhab.binding.emotiva.internal.protocol.EmotivaUdpResponse;
import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The EmotivaProcessorHandler is responsible for handling OpenHAB commands, which are
* sent to one of the channels.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class EmotivaProcessorHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(EmotivaProcessorHandler.class);
private final Map<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;
}
}

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* This class provides translated texts.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
@Component(service = EmotivaTranslationProvider.class)
public class EmotivaTranslationProvider {
private final Bundle bundle;
private final TranslationProvider i18nProvider;
private final LocaleProvider localeProvider;
@Activate
public EmotivaTranslationProvider(@Reference TranslationProvider i18nProvider,
@Reference LocaleProvider localeProvider) {
this.bundle = FrameworkUtil.getBundle(this.getClass());
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
}
public EmotivaTranslationProvider(final EmotivaTranslationProvider other) {
this.bundle = other.bundle;
this.i18nProvider = other.i18nProvider;
this.localeProvider = other.localeProvider;
}
public String getText(String key, @Nullable Object... arguments) {
Locale locale = localeProvider.getLocale();
String message = i18nProvider.getText(bundle, key, this.getDefaultText(key), locale, arguments);
if (message != null) {
return message;
}
return key;
}
public @Nullable String getDefaultText(String key) {
return i18nProvider.getText(bundle, key, key, Locale.ENGLISH);
}
}

View File

@ -0,0 +1,195 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V3;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.emotiva.internal.dto.EmotivaPingDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaTransponderDTO;
import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils;
import org.openhab.core.common.AbstractUID;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service is used for discovering Emotiva devices via sending an EmotivaPing UDP message.
*
* @author Hilbrand Bouwkamp - Initial contribution
* @author Espen Fossen - Adapted to Emotiva binding
*/
@NonNullByDefault
public class EmotivaUdpBroadcastService {
private final Logger logger = LoggerFactory.getLogger(EmotivaUdpBroadcastService.class);
private static final int MAX_PACKET_SIZE = 512;
@Nullable
private DatagramSocket discoverSocket;
private final EmotivaXmlUtils xmlUtils = new EmotivaXmlUtils();
/**
* The address to broadcast EmotivaPing message to.
*/
private final String broadcastAddress;
public EmotivaUdpBroadcastService(String broadcastAddress) throws IllegalArgumentException, JAXBException {
if (broadcastAddress.trim().isEmpty()) {
throw new IllegalArgumentException("Missing broadcast address");
}
this.broadcastAddress = broadcastAddress;
}
/**
* Performs the actual discovery of Emotiva devices (things).
*/
public Optional<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();
}
}

View File

@ -0,0 +1,224 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.emotiva.internal.protocol.EmotivaUdpResponse;
import org.openhab.core.common.NamedThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service is used for receiving UDP message from Emotiva devices.
*
* @author Patrick Koenemann - Initial contribution
* @author Espen Fossen - Adapted to Emotiva binding
*/
@NonNullByDefault
public class EmotivaUdpReceivingService {
private final Logger logger = LoggerFactory.getLogger(EmotivaUdpReceivingService.class);
/**
* Buffer for incoming UDP packages.
*/
private static final int MAX_PACKET_SIZE = 10240;
/**
* The device IP this connector is listening to / sends to.
*/
private final String ipAddress;
/**
* The port this connector is listening to notify message.
*/
private final int receivingPort;
/**
* Service to spawn new threads for handling status updates.
*/
private final ExecutorService executorService;
/**
* Thread factory for UDP listening thread.
*/
private final NamedThreadFactory listeningThreadFactory = new NamedThreadFactory(EmotivaBindingConstants.BINDING_ID,
true);
/**
* Socket for receiving Notify UDP packages.
*/
private @Nullable DatagramSocket receivingSocket = null;
/**
* The listener that gets notified upon newly received messages.
*/
private @Nullable Consumer<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;
}
}

View File

@ -0,0 +1,213 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_UDP_SENDING_TIMEOUT;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionRequest;
import org.openhab.binding.emotiva.internal.dto.EmotivaUnsubscribeDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateRequest;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
import org.openhab.binding.emotiva.internal.protocol.EmotivaUdpResponse;
import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This service handles sending UDP message to Emotiva devices.
*
* @author Patrick Koenemann - Initial contribution
* @author Espen Fossen - Adapted to Emotiva binding
*/
@NonNullByDefault
public class EmotivaUdpSendingService {
private final Logger logger = LoggerFactory.getLogger(EmotivaUdpSendingService.class);
/**
* Buffer for incoming UDP packages.
*/
private static final int MAX_PACKET_SIZE = 10240;
/**
* The device IP this connector is listening to / sends to.
*/
private final String ipAddress;
/**
* The port this connector is sending to.
*/
private final int sendingControlPort;
/**
* Service to spawn new threads for handling status updates.
*/
private final ExecutorService executorService;
/**
* Socket for sending UDP packages.
*/
private @Nullable DatagramSocket sendingSocket = null;
/**
* Sending response listener.
*/
private @Nullable Consumer<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");
}
}
}
}

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.BINDING_ID;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SOURCE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_SOURCE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.EMOTIVA_SOURCE_COMMAND_PREFIX;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class provides the list of valid inputs for a source or audio mode.
*
* @author Kai Kreuzer - Initial contribution
* @author Espen Fossen - Adapted to Emotiva binding
*/
@NonNullByDefault
public class InputStateOptionProvider extends BaseDynamicStateDescriptionProvider implements ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(InputStateOptionProvider.class);
private @Nullable EmotivaProcessorHandler handler;
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = (EmotivaProcessorHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
@Nullable Locale locale) {
ChannelTypeUID typeUID = channel.getChannelTypeUID();
if (typeUID == null || !BINDING_ID.equals(typeUID.getBindingId()) || original == null) {
return null;
}
List<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);
}
}

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.discovery;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*;
import java.util.Objects;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.emotiva.internal.EmotivaUdpBroadcastService;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryService;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Discovery service for Emotiva devices.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.emotiva")
public class EmotivaDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(EmotivaDiscoveryService.class);
@Nullable
private final EmotivaUdpBroadcastService broadcastService = new EmotivaUdpBroadcastService(
DISCOVERY_BROADCAST_ADDRESS);
public EmotivaDiscoveryService() throws IllegalArgumentException, JAXBException {
super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS, false);
}
@Override
protected void startScan() {
logger.debug("Start scan for Emotiva devices");
EmotivaUdpBroadcastService localBroadcastService = broadcastService;
if (localBroadcastService != null) {
try {
localBroadcastService.discoverThings().ifPresent(this::thingDiscovered);
} finally {
removeOlderResults(getTimestampOfLastScan());
}
}
}
@Override
protected void stopScan() {
logger.debug("Stop scan for Emotiva devices");
Objects.requireNonNull(broadcastService).closeDiscoverSocket();
super.stopScan();
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.namespace.QName;
/**
* Defines elements used by common request DTO classes.
*
* @author Espen Fossen - Initial contribution
*/
public class AbstractJAXBElementDTO {
@XmlTransient
protected List<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);
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElement;
/**
* Defines elements used by common notification DTO classes.
*
* @author Espen Fossen - Initial contribution
*/
public class AbstractNotificationDTO {
// Only present with PROTOCOL_V2 or older
@XmlAnyElement(lax = true)
List<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;
}
}

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Emotiva Control XML object, which is part of the {@link EmotivaTransponderDTO} response.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "control")
@XmlAccessorType(XmlAccessType.FIELD)
public class ControlDTO {
@XmlElement(name = "version")
String version;
@XmlElement(name = "controlPort")
int controlPort;
@XmlElement(name = "notifyPort")
int notifyPort;
@XmlElement(name = "infoPort")
int infoPort;
@XmlElement(name = "menuNotifyPort")
int menuNotifyPort;
@XmlElement(name = "setupPortTCP")
int setupPortTCP;
@XmlElement(name = "setupXMLVersion")
int setupXMLVersion;
@XmlElement(name = "keepAlive")
int keepAlive;
public ControlDTO() {
}
public String getVersion() {
return version;
}
public int getControlPort() {
return controlPort;
}
public int getNotifyPort() {
return notifyPort;
}
public int getInfoPort() {
return infoPort;
}
public int getMenuNotifyPort() {
return menuNotifyPort;
}
public int getSetupPortTCP() {
return setupPortTCP;
}
public int getSetupXMLVersion() {
return setupXMLVersion;
}
public int getKeepAlive() {
return keepAlive;
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* The EmotivaAck message type. Received from Emotiva device whenever a {@link EmotivaControlDTO} with
* {@link EmotivaCommandDTO} is sent.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaAck")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaAckDTO {
@XmlAnyElement(lax = true)
private List<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 + '}';
}
}

View File

@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
/**
* The EmotivaBarNotify message type. Received from a device if subscribed to the
* {@link org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags#bar_update} type. Uses the
* {@link EmotivaBarNotifyWrapper} to handle unmarshalling.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "property")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaBarNotifyDTO {
@XmlValue
private String name = "bar";
// Possible values bar, centerBar, bigText, off
@XmlAttribute
private String type;
@XmlAttribute
private String text;
@XmlAttribute
private String units;
@XmlAttribute
private String value;
@XmlAttribute
private String min;
@XmlAttribute
private String max;
@SuppressWarnings("unused")
public EmotivaBarNotifyDTO() {
}
public EmotivaBarNotifyDTO(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getUnits() {
return units;
}
public void setUnits(String units) {
this.units = units;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getMin() {
return min;
}
public void setMin(String min) {
this.min = min;
}
public String getMax() {
return max;
}
public void setMax(String max) {
this.max = max;
}
public String formattedMessage() {
StringBuilder sb = new StringBuilder();
if (type != null) {
if (!"off".equals(type)) {
if (text != null) {
sb.append(text);
}
if (value != null) {
sb.append(" ");
try {
Double doubleValue = Double.valueOf(value);
sb.append(String.format("%.1f", doubleValue));
} catch (NumberFormatException e) {
sb.append(value);
}
}
if (units != null) {
sb.append(" ").append(units);
}
}
}
return sb.toString();
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
/**
* A helper class for receiving {@link EmotivaBarNotifyDTO} messages.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaBarNotify")
public class EmotivaBarNotifyWrapper {
@XmlAttribute
private String sequence;
@XmlAnyElement(lax = true)
List<Object> tags;
@SuppressWarnings("unused")
public EmotivaBarNotifyWrapper() {
}
public String getSequence() {
return sequence;
}
public List<Object> getTags() {
return tags;
}
}

View File

@ -0,0 +1,146 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_CONTROL_ACK_VALUE;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* The EmotivaCommand DTO. Use by multiple message types to control commands in Emotiva devices.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "property")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaCommandDTO {
@XmlValue
private String commandName;
@XmlAttribute
private String value;
@XmlAttribute
private String visible;
@XmlAttribute
private String status;
@XmlAttribute
private String ack;
@SuppressWarnings("unused")
public EmotivaCommandDTO() {
}
public EmotivaCommandDTO(EmotivaControlCommands command) {
this.commandName = command.name();
}
public EmotivaCommandDTO(EmotivaSubscriptionTags tag) {
this.commandName = tag.name();
}
public EmotivaCommandDTO(EmotivaControlCommands command, String value) {
this.commandName = command.name();
this.value = value;
}
public EmotivaCommandDTO(EmotivaControlCommands commandName, String value, String ack) {
this(commandName, value);
this.ack = ack;
}
/**
* Creates a new instance based on command. Primarily used by EmotivaControl messages.
*
* @return EmotivaCommandDTO with ack=yes always added
*/
public static EmotivaCommandDTO fromTypeWithAck(EmotivaControlCommands command) {
EmotivaCommandDTO emotivaCommandDTO = new EmotivaCommandDTO(command);
emotivaCommandDTO.setAck(DEFAULT_CONTROL_ACK_VALUE);
return emotivaCommandDTO;
}
/**
* Creates a new instance based on command and value. Primarily used by EmotivaControl messages.
*
* @return EmotivaCommandDTO with ack=yes always added
*/
public static EmotivaCommandDTO fromTypeWithAck(EmotivaControlCommands command, String value) {
EmotivaCommandDTO emotivaCommandDTO = new EmotivaCommandDTO(command);
if (value != null) {
emotivaCommandDTO.setValue(value);
}
emotivaCommandDTO.setAck(DEFAULT_CONTROL_ACK_VALUE);
return emotivaCommandDTO;
}
public static EmotivaCommandDTO fromType(EmotivaControlCommands command) {
return new EmotivaCommandDTO(command);
}
public static EmotivaCommandDTO fromType(EmotivaSubscriptionTags command) {
return new EmotivaCommandDTO(command);
}
public static EmotivaCommandDTO fromTypeWithAck(EmotivaSubscriptionTags command) {
EmotivaCommandDTO emotivaCommandDTO = new EmotivaCommandDTO(command);
emotivaCommandDTO.setAck(DEFAULT_CONTROL_ACK_VALUE);
return emotivaCommandDTO;
}
public String getName() {
return commandName;
}
public String getValue() {
return value;
}
public String getVisible() {
return visible;
}
public String getStatus() {
return status;
}
public String getAck() {
return ack;
}
public void setName(String name) {
this.commandName = name;
}
public void setValue(String value) {
this.value = value;
}
public void setVisible(String visible) {
this.visible = visible;
}
public void setStatus(String status) {
this.status = status;
}
public void setAck(String ack) {
this.ack = ack;
}
}

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.DEFAULT_CONTROL_MESSAGE_SET_DEFAULT_VALUE;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
/**
* The EmotivaControl message type. Use to send commands via {@link EmotivaCommandDTO} to Emotiva devices.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaControl")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaControlDTO extends AbstractJAXBElementDTO {
@SuppressWarnings("unused")
public EmotivaControlDTO() {
}
public EmotivaControlDTO(List<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)));
}
}

View File

@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Data field use by {@link EmotivaMenuNotifyDTO}.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "col")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaMenuCol {
@XmlAttribute
private String arrow;
@XmlAttribute
private String checkbox;
@XmlAttribute
private String fixed;
@XmlAttribute
private String fixedWidth;
@XmlAttribute
private String highlight;
@XmlAttribute
private String offset;
@XmlAttribute
private String number;
@XmlAttribute
private String value;
public EmotivaMenuCol() {
}
public String getArrow() {
return arrow;
}
public void setArrow(String arrow) {
this.arrow = arrow;
}
public String getCheckbox() {
return checkbox;
}
public void setCheckbox(String checkbox) {
this.checkbox = checkbox;
}
public String getFixed() {
return fixed;
}
public void setFixed(String fixed) {
this.fixed = fixed;
}
public String getFixedWidth() {
return fixedWidth;
}
public void setFixedWidth(String fixedWidth) {
this.fixedWidth = fixedWidth;
}
public String getHighlight() {
return highlight;
}
public void setHighlight(String highlight) {
this.highlight = highlight;
}
public String getOffset() {
return offset;
}
public void setOffset(String offset) {
this.offset = offset;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* The EmotivaMenuNotify message type. Received from a device if subscribed to the
*
* @link EmotivaSubscriptionTags#menu_update} type.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaMenuNotify")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaMenuNotifyDTO {
@XmlAttribute
private String sequence;
@XmlElement
private List<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;
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Data field use by {@link EmotivaMenuNotifyDTO}.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "progress")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaMenuProgress {
@XmlAttribute
private String time;
public EmotivaMenuProgress() {
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Data field use by {@link EmotivaMenuNotifyDTO}.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "row")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaMenuRow {
@XmlAttribute
private String number;
@XmlElement
private List<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;
}
}

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
/**
* The EmotivaNotify message type. Received from a device if subscribed to
* {@link org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags} values. Uses
* the {@link EmotivaNotifyWrapper} to handle unmarshalling.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "property")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaNotifyDTO {
@XmlValue
private String tagName;
@XmlAttribute
private String value;
@XmlAttribute
private String visible;
@XmlAttribute
private String status;
@XmlAttribute
private String ack;
@SuppressWarnings("unused")
public EmotivaNotifyDTO() {
}
public EmotivaNotifyDTO(String tag) {
this.tagName = tag;
}
public String getName() {
return tagName;
}
public String getValue() {
return value;
}
public String getVisible() {
return visible;
}
public String getStatus() {
return status;
}
public void setName(String name) {
this.tagName = name;
}
public void setValue(String value) {
this.value = value;
}
public void setVisible(String visible) {
this.visible = visible;
}
public void setStatus(String status) {
this.status = status;
}
public String getAck() {
return ack;
}
public void setAck(String ack) {
this.ack = ack;
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Emotiva Notify message type. 2.x version of protocol uses command type as prefix in each line in the body, while 3.x
* users property as prefix with name="commandType". 2.x is handled as a element with a special handler unmarshall
* handler in {@link org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils}, while 3.x qualifies as a proper xml
* element and can be properly unmarshalled by
* JAXB.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaNotify")
public class EmotivaNotifyWrapper extends AbstractNotificationDTO {
@XmlAttribute
private String sequence;
@SuppressWarnings("unused")
public EmotivaNotifyWrapper() {
}
public EmotivaNotifyWrapper(String sequence, List<EmotivaPropertyDTO> properties) {
this.sequence = sequence;
this.properties = properties;
}
public String getSequence() {
return sequence;
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
/**
* The EmotivaPing message type. Use to discover Emotiva devices.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaPing")
public class EmotivaPingDTO {
@XmlAttribute
private String protocol;
public EmotivaPingDTO() {
}
public EmotivaPingDTO(String protocol) {
this.protocol = protocol;
}
public String getProtocol() {
return protocol;
}
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
/**
* The EmotivaProperty DTO. Use by multiple message types to get updates from Emotiva devices.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "property")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaPropertyDTO {
@XmlAttribute
private String name;
@XmlAttribute
private String value;
@XmlAttribute
private String visible;
@XmlAttribute
private String status;
@SuppressWarnings("unused")
public EmotivaPropertyDTO() {
}
public EmotivaPropertyDTO(String name, String value, String visible) {
this.name = name;
this.value = value;
this.visible = visible;
}
public EmotivaPropertyDTO(String name, String value, String visible, String status) {
this.name = name;
this.value = value;
this.visible = visible;
this.status = status;
}
public String getName() {
return name;
}
public String getValue() {
return Objects.requireNonNullElse(value, "");
}
public String getVisible() {
return Objects.requireNonNullElse(visible, "false");
}
public String getStatus() {
return Objects.requireNonNullElse(status, "");
}
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlValue;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* The EmotivaSubscriptionDTO message type. Used to send commands via {@link EmotivaSubscriptionRequest} to Emotiva
* devices.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "property")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaSubscriptionDTO {
@XmlValue
private String propertyName;
@SuppressWarnings("unused")
public EmotivaSubscriptionDTO() {
}
public EmotivaSubscriptionDTO(EmotivaSubscriptionTags property) {
this.propertyName = property.name();
}
public static EmotivaSubscriptionDTO fromType(EmotivaSubscriptionTags tag) {
return new EmotivaSubscriptionDTO(tag);
}
public String getName() {
return propertyName;
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V2;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* A helper class for sending {@link EmotivaSubscriptionDTO} messages.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaSubscription")
public class EmotivaSubscriptionRequest extends AbstractJAXBElementDTO {
@XmlAttribute
private String protocol = PROTOCOL_V2.value();
@SuppressWarnings("unused")
public EmotivaSubscriptionRequest() {
}
public EmotivaSubscriptionRequest(List<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));
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* A helper class for receiving {@link EmotivaSubscriptionDTO} messages.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaSubscription")
public class EmotivaSubscriptionResponse {
// Only present with PROTOCOL_V2 or older
@XmlAnyElement(lax = true)
List<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;
}
}

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* The EmotivaTransponder message type. Received from a device if after a successful device discovery via the
* {@link EmotivaPingDTO} message type.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaTransponder")
public class EmotivaTransponderDTO {
@XmlElement(name = "model")
private String model;
@XmlElement(name = "revision")
private String revision;
@XmlElement(name = "dataRevision")
private String dataRevision;
@XmlElement(name = "name")
private String name;
@XmlElement(name = "control")
private ControlDTO control;
public java.lang.String getModel() {
return model;
}
public java.lang.String getRevision() {
return revision;
}
public String getDataRevision() {
return dataRevision;
}
public String getName() {
return name;
}
public ControlDTO getControl() {
return control;
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlRootElement;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* The EmotivaUnsubscriptionDTO message type. Use to remove subscription after registration via {
*
* @link EmotivaSubscriptionRequest}.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaUnsubscribe")
public class EmotivaUnsubscribeDTO extends AbstractJAXBElementDTO {
@SuppressWarnings("unused")
public EmotivaUnsubscribeDTO() {
}
public EmotivaUnsubscribeDTO(List<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));
}
}

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* A helper class for sending EmotivaUpdate messages with {@link EmotivaCommandDTO} commands.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaUpdate")
public class EmotivaUpdateRequest extends AbstractJAXBElementDTO {
@XmlAttribute
private String protocol;
@SuppressWarnings("unused")
public EmotivaUpdateRequest() {
}
public EmotivaUpdateRequest(List<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));
}
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
/**
* The EmotivaUpdate message type. Received if an {@link EmotivaUpdateRequest} sent to a device.
*
* @author Espen Fossen - Initial contribution
*/
@XmlRootElement(name = "emotivaUpdate")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmotivaUpdateResponse extends AbstractNotificationDTO {
@SuppressWarnings("unused")
public EmotivaUpdateResponse() {
}
public EmotivaUpdateResponse(List<EmotivaPropertyDTO> properties) {
this.properties = properties;
}
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enum types for commands to send to Emotiva devices. Used by {@link EmotivaControlRequest} to create correct
* {@link org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO} command message.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public enum EmotivaCommandType {
CYCLE, // Cycles to multiple states
NONE, // Unknown or not in use commands
NUMBER,
MENU_CONTROL,
MODE, // Audio mode
SET, // Sets a specific number or string value
SOURCE_MAIN_ZONE, // Main Zone sources
SOURCE_USER, // Source with possible user assigned name
SOURCE_ZONE2, // Zone 2 sources
SPEAKER_PRESET, // Speaker preset
TOGGLE, // Two state toggle
UP_DOWN_SINGLE, // +1/-1
UP_DOWN_HALF // +0.5/-0.5
}

View File

@ -0,0 +1,240 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.CYCLE;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.MENU_CONTROL;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.MODE;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.NONE;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.NUMBER;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SET;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SOURCE_MAIN_ZONE;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SOURCE_USER;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SOURCE_ZONE2;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.SPEAKER_PRESET;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.TOGGLE;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.UP_DOWN_HALF;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.UP_DOWN_SINGLE;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.DIMENSIONLESS_DECIBEL;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.DIMENSIONLESS_PERCENT;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.NOT_IMPLEMENTED;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.ON_OFF;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.STRING;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.UNKNOWN;
import java.util.EnumMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Emotiva command name with corresponding command type and UoM data type.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public enum EmotivaControlCommands {
none("", NONE, UNKNOWN),
standby("", TOGGLE, ON_OFF),
source_tuner("Tuner", SOURCE_USER, STRING),
source_1("Input 1", SOURCE_USER, STRING),
source_2("Input 2", SOURCE_USER, STRING),
source_3("Input 3", SOURCE_USER, STRING),
source_4("Input 4", SOURCE_USER, STRING),
source_5("Input 5", SOURCE_USER, STRING),
source_6("Input 6", SOURCE_USER, STRING),
source_7("Input 7", SOURCE_USER, STRING),
source_8("Input 8", SOURCE_USER, STRING),
menu("", SET, STRING),
menu_control("", MENU_CONTROL, STRING), // Not an Emotiva command, just a placeholder
up("", SET, STRING),
down("", SET, STRING),
left("", SET, STRING),
right("", SET, STRING),
enter("", SET, STRING),
dim("", CYCLE, DIMENSIONLESS_PERCENT),
mode("", MODE, STRING),
info("", SET, UNKNOWN),
mute("", SET, ON_OFF),
mute_off("", SET, ON_OFF),
mute_on("", SET, ON_OFF),
music("", SET, STRING),
movie("", SET, STRING),
center("", SET, DIMENSIONLESS_DECIBEL),
subwoofer("", SET, DIMENSIONLESS_DECIBEL),
surround("", SET, DIMENSIONLESS_DECIBEL),
back("", SET, DIMENSIONLESS_DECIBEL),
input("", NONE, STRING),
input_up("", SET, STRING),
input_down("", SET, STRING),
power("", TOGGLE, ON_OFF), // Not an Emotiva command, just a placeholder
power_on("", SET, ON_OFF),
power_off("", SET, ON_OFF),
volume("", SET, DIMENSIONLESS_DECIBEL),
set_volume("", NUMBER, DIMENSIONLESS_DECIBEL),
loudness_on("", SET, ON_OFF),
loudness_off("", SET, ON_OFF),
loudness("", TOGGLE, ON_OFF),
speaker_preset("", SPEAKER_PRESET, STRING),
mode_up("", SET, STRING),
mode_down("", SET, STRING),
bass("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL), // Not an Emotiva command, just a placeholder
bass_up("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL),
bass_down("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL),
treble("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL), // Not an Emotiva command, just a placeholder
treble_up("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL),
treble_down("", UP_DOWN_HALF, DIMENSIONLESS_DECIBEL),
zone2_power("", TOGGLE, ON_OFF),
zone2_power_off("", SET, ON_OFF),
zone2_power_on("", SET, ON_OFF),
zone2_volume("", SET, DIMENSIONLESS_DECIBEL),
zone2_set_volume("", NUMBER, STRING),
zone2_input("", NONE, STRING),
zone1_band("", TOGGLE, STRING),
band_am("", SET, STRING),
band_fm("", SET, STRING),
zone2_mute("", TOGGLE, ON_OFF),
zone2_mute_off("", SET, ON_OFF),
zone2_mute_on("", SET, ON_OFF),
zone2_band("", SET, NOT_IMPLEMENTED),
frequency("", UP_DOWN_SINGLE, ON_OFF),
seek("", UP_DOWN_SINGLE, ON_OFF),
channel("", UP_DOWN_SINGLE, ON_OFF),
stereo("", SET, STRING),
direct("", SET, STRING),
dolby("", SET, STRING),
dts("", SET, STRING),
all_stereo("", SET, STRING),
auto("", SET, STRING),
reference_stereo("", SET, STRING),
surround_mode("", SET, STRING),
preset1("Preset 1", SET, STRING),
preset2("Preset 2", SET, STRING),
dirac("Dirac", SET, STRING),
hdmi1("HDMI 1", SOURCE_MAIN_ZONE, STRING),
hdmi2("HDMI 2", SOURCE_MAIN_ZONE, STRING),
hdmi3("HDMI 3", SOURCE_MAIN_ZONE, STRING),
hdmi4("HDMI 4", SOURCE_MAIN_ZONE, STRING),
hdmi5("HDMI 5", SOURCE_MAIN_ZONE, STRING),
hdmi6("HDMI 6", SOURCE_MAIN_ZONE, STRING),
hdmi7("HDMI 7", SOURCE_MAIN_ZONE, STRING),
hdmi8("HDMI 8", SOURCE_MAIN_ZONE, STRING),
analog1("Analog 1", SOURCE_MAIN_ZONE, STRING),
analog2("Analog 2", SOURCE_MAIN_ZONE, STRING),
analog3("Analog 3", SOURCE_MAIN_ZONE, STRING),
analog4("Analog 4", SOURCE_MAIN_ZONE, STRING),
analog5("Analog 5", SOURCE_MAIN_ZONE, STRING),
analog71("Analog 7.1", SOURCE_MAIN_ZONE, STRING),
ARC("Audio Return Channel", SOURCE_MAIN_ZONE, STRING),
coax1("Coax 1", SOURCE_MAIN_ZONE, STRING),
coax2("Coax 2", SOURCE_MAIN_ZONE, STRING),
coax3("Coax 3", SOURCE_MAIN_ZONE, STRING),
coax4("Coax 4", SOURCE_MAIN_ZONE, STRING),
front_in("Front In", SOURCE_MAIN_ZONE, STRING),
optical1("Optical 1", SOURCE_MAIN_ZONE, STRING),
optical2("Optical 2", SOURCE_MAIN_ZONE, STRING),
optical3("Optical 3", SOURCE_MAIN_ZONE, STRING),
optical4("Optical 4", SOURCE_MAIN_ZONE, STRING),
tuner("Tuner 1", SOURCE_MAIN_ZONE, STRING),
usb_stream("USB Stream", SOURCE_MAIN_ZONE, STRING),
center_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL),
subwoofer_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL),
surround_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL),
back_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL),
width_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL),
height_trim_set("", NUMBER, DIMENSIONLESS_DECIBEL),
zone2_analog1("Analog 1", SOURCE_ZONE2, STRING),
zone2_analog2("Analog 2", SOURCE_ZONE2, STRING),
zone2_analog3("Analog 3", SOURCE_ZONE2, STRING),
zone2_analog4("Analog 4", SOURCE_ZONE2, STRING),
zone2_analog5("Analog 5", SOURCE_ZONE2, STRING),
zone2_analog71("Analog 7.1", SOURCE_ZONE2, STRING),
zone2_analog8("Analog 8", SOURCE_ZONE2, STRING),
zone2_ARC("Audio Return Channel", SOURCE_ZONE2, STRING),
zone2_coax1("Coax 1", SOURCE_ZONE2, STRING),
zone2_coax2("Coax 2", SOURCE_ZONE2, STRING),
zone2_coax3("Coax 3", SOURCE_ZONE2, STRING),
zone2_coax4("Coax 4", SOURCE_ZONE2, STRING),
zone2_ethernet("Ethernet", SOURCE_ZONE2, STRING),
zone2_follow_main("Follow Main", SOURCE_ZONE2, STRING),
zone2_front_in("Front In", SOURCE_ZONE2, STRING),
zone2_optical1("Optical 1", SOURCE_ZONE2, STRING),
zone2_optical2("Optical 2", SOURCE_ZONE2, STRING),
zone2_optical3("Optical 3", SOURCE_ZONE2, STRING),
zone2_optical4("Optical 4", SOURCE_ZONE2, STRING),
channel_1("Channel 1", SET, STRING),
channel_2("Channel 2", SET, STRING),
channel_3("Channel 3", SET, STRING),
channel_4("Channel 4", SET, STRING),
channel_5("Channel 5", SET, STRING),
channel_6("Channel 6", SET, STRING),
channel_7("Channel 7", SET, STRING),
channel_8("Channel 8", SET, STRING),
channel_9("Channel 9", SET, STRING),
channel_10("Channel 10", SET, STRING),
channel_11("Channel 11", SET, STRING),
channel_12("Channel 12", SET, STRING),
channel_13("Channel 13", SET, STRING),
channel_14("Channel 14", SET, STRING),
channel_15("Channel 15", SET, STRING),
channel_16("Channel 16", SET, STRING),
channel_17("Channel 17", SET, STRING),
channel_18("Channel 18", SET, STRING),
channel_19("Channel 19", SET, STRING),
channel_20("Channel 20", SET, STRING);
private final String label;
private final EmotivaCommandType commandType;
private final EmotivaDataType dataType;
EmotivaControlCommands(String label, EmotivaCommandType commandType, EmotivaDataType dataType) {
this.label = label;
this.commandType = commandType;
this.dataType = dataType;
}
public static EmotivaControlCommands matchToInput(String inputName) {
for (EmotivaControlCommands value : values()) {
if (inputName.toLowerCase().equals(value.name())) {
return value;
}
}
if (inputName.startsWith("input_")) {
return valueOf(inputName.replace("input_", "source_"));
}
return none;
}
public String getLabel() {
return label;
}
public EmotivaCommandType getCommandType() {
return commandType;
}
public EmotivaDataType getDataType() {
return dataType;
}
public static EnumMap<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;
}
}

View File

@ -0,0 +1,475 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.clamp;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumePercentageToDecibel;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaCommandType.*;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.FREQUENCY_HERTZ;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_band;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_channel;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Binds channels to a given command with datatype.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class EmotivaControlRequest {
private final Logger logger = LoggerFactory.getLogger(EmotivaControlRequest.class);
private String name;
private final EmotivaDataType dataType;
private String channel;
private final EmotivaControlCommands defaultCommand;
private final EmotivaControlCommands setCommand;
private final EmotivaControlCommands onCommand;
private final EmotivaControlCommands offCommand;
private final EmotivaControlCommands upCommand;
private final EmotivaControlCommands downCommand;
private double maxValue;
private double minValue;
private final Map<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 + '}';
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This enum is used to describe the value types from Emotiva.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public enum EmotivaDataType {
DIMENSIONLESS_DECIBEL("decibel"),
DIMENSIONLESS_PERCENT("percent"),
FREQUENCY_HERTZ("hertz"),
NUMBER("number"),
NUMBER_TIME("number_time"),
GOODBYE("goodbye"),
NOT_IMPLEMENTED("not_implemented"),
ON_OFF("boolean"),
STRING("string"),
UNKNOWN("unknown");
private final String name;
EmotivaDataType(String name) {
this.name = name;
}
public static EmotivaDataType fromName(String name) {
EmotivaDataType result = EmotivaDataType.UNKNOWN;
for (EmotivaDataType m : EmotivaDataType.values()) {
if (m.name.equals(name)) {
result = m;
break;
}
}
return result;
}
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Status types for status fields of different message type.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public enum EmotivaPropertyStatus {
VALID("ack"),
NOT_VALID("nak");
private final String value;
EmotivaPropertyStatus(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enum for mapping Emotiva Network Protocol versions.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public enum EmotivaProtocolVersion {
PROTOCOL_V2("2.0"),
PROTOCOL_V3("3.0");
private final String protocolVersion;
EmotivaProtocolVersion(String protocolVersion) {
this.protocolVersion = protocolVersion;
}
public static EmotivaProtocolVersion protocolFromConfig(String protocolVersion) {
for (EmotivaProtocolVersion value : values()) {
if (protocolVersion.equals(value.protocolVersion)) {
return value;
}
}
return PROTOCOL_V2;
}
public String value() {
return protocolVersion;
}
}

View File

@ -0,0 +1,186 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.*;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Emotiva subscription tags with corresponding UoM data type and channel.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public enum EmotivaSubscriptionTags {
/* Protocol V1 notify tags */
power("power", ON_OFF, CHANNEL_MAIN_ZONE_POWER),
source("source", STRING, CHANNEL_SOURCE),
dim("dim", DIMENSIONLESS_PERCENT, CHANNEL_DIM),
mode("mode", STRING, CHANNEL_MODE),
speaker_preset("speaker-preset", STRING, CHANNEL_SPEAKER_PRESET),
center("center", DIMENSIONLESS_DECIBEL, CHANNEL_CENTER),
subwoofer("subwoofer", DIMENSIONLESS_DECIBEL, CHANNEL_SUBWOOFER),
surround("surround", DIMENSIONLESS_DECIBEL, CHANNEL_SURROUND),
back("back", DIMENSIONLESS_DECIBEL, CHANNEL_BACK),
volume("volume", DIMENSIONLESS_DECIBEL, CHANNEL_MAIN_VOLUME),
loudness("loudness", ON_OFF, CHANNEL_LOUDNESS),
treble("treble", DIMENSIONLESS_DECIBEL, CHANNEL_TREBLE),
bass("bass", DIMENSIONLESS_DECIBEL, CHANNEL_BASS),
zone2_power("zone2-power", ON_OFF, CHANNEL_ZONE2_POWER),
zone2_volume("zone2-volume", DIMENSIONLESS_DECIBEL, CHANNEL_ZONE2_VOLUME),
zone2_input("zone2-input", STRING, CHANNEL_ZONE2_SOURCE),
tuner_band("tuner-band", STRING, CHANNEL_TUNER_BAND),
tuner_channel("tuner-channel", FREQUENCY_HERTZ, CHANNEL_TUNER_CHANNEL),
tuner_signal("tuner-signal", STRING, CHANNEL_TUNER_SIGNAL),
tuner_program("tuner-program", STRING, CHANNEL_TUNER_PROGRAM),
tuner_RDS("tuner-RDS", STRING, CHANNEL_TUNER_RDS),
audio_input("audio-input", STRING, CHANNEL_AUDIO_INPUT),
audio_bitstream("audio-bitstream", STRING, CHANNEL_AUDIO_BITSTREAM),
audio_bits("audio-bits", STRING, CHANNEL_AUDIO_BITS),
video_input("video-input", STRING, CHANNEL_VIDEO_INPUT),
video_format("video-format", STRING, CHANNEL_VIDEO_FORMAT),
video_space("video-space", STRING, CHANNEL_VIDEO_SPACE),
input_1("input-1", STRING, CHANNEL_INPUT1),
input_2("input-2", STRING, CHANNEL_INPUT2),
input_3("input-3", STRING, CHANNEL_INPUT3),
input_4("input-4", STRING, CHANNEL_INPUT4),
input_5("input-5", STRING, CHANNEL_INPUT5),
input_6("input-6", STRING, CHANNEL_INPUT6),
input_7("input-7", STRING, CHANNEL_INPUT7),
input_8("input-8", STRING, CHANNEL_INPUT8),
/* Protocol V2 notify tags */
selected_mode("selected-mode", STRING, CHANNEL_SELECTED_MODE),
selected_movie_music("selected-movie-music", STRING, CHANNEL_SELECTED_MOVIE_MUSIC),
mode_ref_stereo("mode-ref-stereo", STRING, CHANNEL_MODE_REF_STEREO),
mode_stereo("mode-stereo", STRING, CHANNEL_MODE_STEREO),
mode_music("mode-music", STRING, CHANNEL_MODE_MUSIC),
mode_movie("mode-movie", STRING, CHANNEL_MODE_MOVIE),
mode_direct("mode-direct", STRING, CHANNEL_MODE_DIRECT),
mode_dolby("mode-dolby", STRING, CHANNEL_MODE_DOLBY),
mode_dts("mode-dts", STRING, CHANNEL_MODE_DTS),
mode_all_stereo("mode-all-stereo", STRING, CHANNEL_MODE_ALL_STEREO),
mode_auto("mode-auto", STRING, CHANNEL_MODE_AUTO),
mode_surround("mode-surround", STRING, CHANNEL_MODE_SURROUND),
menu("menu", ON_OFF, CHANNEL_MENU),
menu_update("menu-update", STRING, CHANNEL_MENU_DISPLAY_PREFIX),
/* Protocol V3 notify tags */
keepAlive("keepAlive", NUMBER_TIME, LAST_SEEN_STATE_NAME),
goodBye("goodBye", GOODBYE, ""),
bar_update("bar-update", STRING, CHANNEL_BAR),
width("width", DIMENSIONLESS_DECIBEL, CHANNEL_WIDTH),
height("height", DIMENSIONLESS_DECIBEL, CHANNEL_HEIGHT),
/* Notify tag not in the documentation */
source_tuner("source-tuner", ON_OFF, ""),
/* No match tag */
unknown("unknown", UNKNOWN, "");
private final Logger logger = LoggerFactory.getLogger(EmotivaSubscriptionTags.class);
/* For error handling */
public static final String UNKNOWN_TAG = "unknown";
private final String name;
private final EmotivaDataType dataType;
private final String channel;
EmotivaSubscriptionTags(String name, EmotivaDataType dataType, String channel) {
this.name = name;
this.dataType = dataType;
this.channel = channel;
}
public static boolean hasChannel(String name) {
try {
EmotivaSubscriptionTags type = EmotivaSubscriptionTags.valueOf(name);
if (!type.channel.isEmpty()) {
return true;
}
} catch (IllegalArgumentException e) {
// do nothing
}
return false;
}
public static EmotivaSubscriptionTags fromChannelUID(String id) {
for (EmotivaSubscriptionTags value : values()) {
if (id.equals(value.getChannel())) {
return value;
}
}
return EmotivaSubscriptionTags.unknown;
}
public static EmotivaSubscriptionTags[] generalChannels() {
List<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;
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
/**
* The class {@link EmotivaUdpResponse} represents UDP response we expect.
*
* @author Andi Bräu - Initial contribution
* @author Espen Fossen - Adpated to Emotiva binding
*/
public record EmotivaUdpResponse(String answer, String ipAddress) {
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EmotivaUdpResponse that = (EmotivaUdpResponse) o;
return answer.equals(that.answer) && ipAddress.equals(that.ipAddress);
}
}

View File

@ -0,0 +1,298 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.UNKNOWN_TAG;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.emotiva.internal.dto.AbstractJAXBElementDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaAckDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaBarNotifyDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaBarNotifyWrapper;
import org.openhab.binding.emotiva.internal.dto.EmotivaCommandDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaMenuNotifyDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyWrapper;
import org.openhab.binding.emotiva.internal.dto.EmotivaPingDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaPropertyDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionRequest;
import org.openhab.binding.emotiva.internal.dto.EmotivaSubscriptionResponse;
import org.openhab.binding.emotiva.internal.dto.EmotivaTransponderDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaUnsubscribeDTO;
import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateRequest;
import org.openhab.binding.emotiva.internal.dto.EmotivaUpdateResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
/**
* Helper class for marshalling and unmarshalling Emotiva message types.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class EmotivaXmlUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(EmotivaXmlUtils.class);
Marshaller marshaller;
JAXBContext context;
public EmotivaXmlUtils() throws JAXBException {
context = JAXBContext.newInstance(EmotivaAckDTO.class, EmotivaBarNotifyWrapper.class, EmotivaBarNotifyDTO.class,
EmotivaCommandDTO.class, EmotivaControlDTO.class, EmotivaMenuNotifyDTO.class,
EmotivaNotifyWrapper.class, EmotivaPingDTO.class, EmotivaPropertyDTO.class,
EmotivaSubscriptionRequest.class, EmotivaSubscriptionResponse.class, EmotivaTransponderDTO.class,
EmotivaUnsubscribeDTO.class, EmotivaUpdateRequest.class, EmotivaUpdateResponse.class);
marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
}
public String marshallEmotivaDTO(Object objectInstanceType) {
try {
StringWriter out = new StringWriter();
marshaller.marshal(objectInstanceType, out);
return out.toString();
} catch (JAXBException e) {
LOGGER.debug("Could not marshall class of type {}", objectInstanceType.getClass().getName(), e);
}
return "";
}
public String marshallJAXBElementObjects(AbstractJAXBElementDTO jaxbElementDTO) {
try {
StringWriter out = new StringWriter();
List<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;
}
}

View File

@ -0,0 +1,115 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_CHANNEL;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_FREQUENCY;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_HEIGHT;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MAIN_VOLUME;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MAIN_VOLUME_DB;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_CONTROL;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_DOWN;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_ENTER;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_LEFT;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_RIGHT;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MENU_UP;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_ALL_STEREO;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_AUTO;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_DIRECT;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_DOLBY;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_DTS;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_MOVIE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_MUSIC;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_REF_STEREO;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_STEREO;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MODE_SURROUND;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MUTE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SEEK;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SOURCE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_STANDBY;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SURROUND_MODE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_WIDTH;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_MUTE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_SOURCE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_ZONE2_VOLUME_DB;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Maps OH channels with only an indirect connection to an Emotiva command. Only handles 1:1 mappings.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public enum OHChannelToEmotivaCommand {
standby(CHANNEL_STANDBY, EmotivaControlCommands.standby),
source(CHANNEL_SOURCE, EmotivaControlCommands.input),
menu(CHANNEL_MENU, EmotivaControlCommands.menu),
menu_control(CHANNEL_MENU_CONTROL, EmotivaControlCommands.menu_control),
up(CHANNEL_MENU_UP, EmotivaControlCommands.up),
down(CHANNEL_MENU_DOWN, EmotivaControlCommands.down),
left(CHANNEL_MENU_LEFT, EmotivaControlCommands.left),
right(CHANNEL_MENU_RIGHT, EmotivaControlCommands.right),
enter(CHANNEL_MENU_ENTER, EmotivaControlCommands.enter),
mute(CHANNEL_MUTE, EmotivaControlCommands.mute),
volume(CHANNEL_MAIN_VOLUME, EmotivaControlCommands.volume),
volume_db(CHANNEL_MAIN_VOLUME_DB, EmotivaControlCommands.volume),
zone2_volume(CHANNEL_ZONE2_VOLUME, EmotivaControlCommands.zone2_volume),
zone2_volume_db(CHANNEL_ZONE2_VOLUME_DB, EmotivaControlCommands.zone2_volume),
zone2_mute(CHANNEL_ZONE2_MUTE, EmotivaControlCommands.zone2_mute),
zone2_source(CHANNEL_ZONE2_SOURCE, EmotivaControlCommands.zone2_input),
width(CHANNEL_WIDTH, EmotivaControlCommands.width_trim_set),
height(CHANNEL_HEIGHT, EmotivaControlCommands.height_trim_set),
frequency(CHANNEL_FREQUENCY, EmotivaControlCommands.frequency),
seek(CHANNEL_SEEK, EmotivaControlCommands.seek),
channel(CHANNEL_CHANNEL, EmotivaControlCommands.channel),
mode_ref_stereo(CHANNEL_MODE_REF_STEREO, EmotivaControlCommands.reference_stereo),
surround_mode(CHANNEL_SURROUND_MODE, EmotivaControlCommands.surround_mode),
mode_surround(CHANNEL_MODE_SURROUND, EmotivaControlCommands.surround_mode),
mode_stereo(CHANNEL_MODE_STEREO, EmotivaControlCommands.stereo),
mode_music(CHANNEL_MODE_MUSIC, EmotivaControlCommands.music),
mode_movie(CHANNEL_MODE_MOVIE, EmotivaControlCommands.movie),
mode_direct(CHANNEL_MODE_DIRECT, EmotivaControlCommands.direct),
mode_dolby(CHANNEL_MODE_DOLBY, EmotivaControlCommands.dolby),
mode_dts(CHANNEL_MODE_DTS, EmotivaControlCommands.dts),
mode_all_stereo(CHANNEL_MODE_ALL_STEREO, EmotivaControlCommands.all_stereo),
mode_auto(CHANNEL_MODE_AUTO, EmotivaControlCommands.auto);
private final String ohChannel;
private final EmotivaControlCommands command;
OHChannelToEmotivaCommand(String ohChannel, EmotivaControlCommands command) {
this.ohChannel = ohChannel;
this.command = command;
}
public String getChannel() {
return ohChannel;
}
public EmotivaControlCommands getCommand() {
return command;
}
public static EmotivaControlCommands fromChannelUID(String id) {
for (OHChannelToEmotivaCommand value : values()) {
if (id.equals(value.ohChannel)) {
return value.command;
}
}
return EmotivaControlCommands.none;
}
}

View File

@ -0,0 +1,31 @@
<?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>

View File

@ -0,0 +1,61 @@
<?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>

View File

@ -0,0 +1,201 @@
addon.emotiva.name = Emotiva Binding
addon.emotiva.description = This is the binding for Emotiva Audio Corporation AV processors.
# thing types
thing-type.emotiva.processor.label = Processor
thing-type.emotiva.processor.description = Control a Emotiva AV Processor.
thing-type.emotiva.processor.group.main-zone.label = Main Zone Control
thing-type.emotiva.processor.group.main-zone.description = Channels for the main zone of this device.
thing-type.emotiva.processor.group.zone2.label = Zone 2 Control
thing-type.emotiva.processor.group.zone2.description = Channels for Zone 2 of this device.
# thing types config
thing-type.config.emotiva.config.ipAddress.label = IP address
thing-type.config.emotiva.config.ipAddress.description = IP address of the device
thing-type.config.emotiva.config.controlPort = Control Port
thing-type.config.emotiva.config.controlPort.description = UDP port to send commands to the device
thing-type.config.emotiva.config.notifyPort.label = Notify Port
thing-type.config.emotiva.config.notifyPort.description = UDP port to receive notifications from the device
thing-type.config.emotiva.config.infoPort.label = Info Port
thing-type.config.emotiva.config.infoPort.description = UDP port
thing-type.config.emotiva.config.setupPortTCP.label = Setup TCP Port
thing-type.config.emotiva.config.setupPortTCP.description = TCP port for remote setup
thing-type.config.emotiva.config.menuNotifyPort.label = Menu Notify Port
thing-type.config.emotiva.config.menuNotifyPort.description = UDP port to receive menu notifications from the device
thing-type.config.emotiva.config.protocolVersion.label = Emotiva Protocol Version
thing-type.config.emotiva.config.protocolVersion.description = Emotiva Network Remote Control protocol version
thing-type.config.emotiva.config.keepAlive.label = Keep Alive Notification
thing-type.config.emotiva.config.keepAlive.description = The interval, in milliseconds, at which the Emotiva Device will send a "keepAlive" notification
# channel group types
channel-group-type.emotiva.general.label = General Control
channel-group-type.emotiva.general.description = General channels for this device.
channel-group-type.emotiva.zone.label = Zone Control
channel-group-type.emotiva.zone.description = Channels for a zone of this device.
# channel types
channel-type.emotiva.audio-input.label = Audio Input
channel-type.emotiva.audio-input.description = Source for audio input
channel-type.emotiva.audio-bitstream.label = Audio Input Bitstream Type
channel-type.emotiva.audio-bitstream.description = Current audio bitstream, "PCM 2.0", "ATMOS", etc.
channel-type.emotiva.audio-bits.label = Audio Input Bits
channel-type.emotiva.audio-bits.description = Current audio input bits: "32kHZ 24bits", etc.
channel-type.emotiva.bar.label = Front Panel Bar
channel-type.emotiva.bar.description = Displays text from the front panel bar of the device
channel-type.emotiva.channel.label = Radio Tuner Channel
channel-type.emotiva.channel.description = Changes radio tuner channel a station at a time, up or down
channel-type.emotiva.frequency.label = Radio Tuner Frequency
channel-type.emotiva.frequency.description = Changes radio tuner frequency, up or down
channel-type.emotiva.dim.label = Front Panel Dimness
channel-type.emotiva.dim.description = Percentage of light on front panel
channel-type.emotiva.input-name.label = Input Name
channel-type.emotiva.input-name.description = User assigned name for input or mode
channel-type.emotiva.loudness.label = Loudness
channel-type.emotiva.loudness.description = Loudness ON/OFF
channel-type.emotiva.mainPower.label = Power
channel-type.emotiva.mainPower.description = Power ON/OFF the device
channel-type.emotiva.menu.label = Menu
channel-type.emotiva.menu.description = Controls the device menu
channel-type.emotiva.mode.label = Mode
channel-type.emotiva.mode.description = Sets main zone mode, "Stereo", "Direct", "Auto", etc.
channel-type.emotiva.mode-surround.label = Surround Mode
channel-type.emotiva.mode-surround.description = Select the surround mode for this zone of the device
channel-type.emotiva.mute.label = Mute
channel-type.emotiva.mute.description = Enable/Disable Mute on this zone of the device
channel-type.emotiva.seek.label = Radio Tuner Seek
channel-type.emotiva.seek.description = Enables seek of radio channel, up or down
channel-type.emotiva.selected-mode.label = Selected Mode
channel-type.emotiva.selected-mode.description = User selected mode for the main zone. An "Auto" value here might not mean the mode channel is in auto.
channel-type.emotiva.selected-mode.state.option.all-stereo = All Stereo
channel-type.emotiva.selected-mode.state.option.auto = Auto
channel-type.emotiva.selected-mode.state.option.direct = Direct
channel-type.emotiva.selected-mode.state.option.dolby = Dolby
channel-type.emotiva.selected-mode.state.option.dts = DTS
channel-type.emotiva.selected-mode.state.option.stereo = Stereo
channel-type.emotiva.selected-mode.state.option.surround = Surround
channel-type.emotiva.selected-mode.state.option.ref-stereo = Reference Stereo
channel-type.emotiva.selected-movie-music.label = Selected Movie Music
channel-type.emotiva.selected-movie-music.description = User-selected movie or music mode for main zone: "Movie" or "Music".
channel-type.emotiva.selected-movie-music.state.option.movie = Movie
channel-type.emotiva.selected-movie-music.state.option.music = Music
channel-type.emotiva.speaker-preset.label = Speaker Preset
channel-type.emotiva.speaker-preset.description = Speaker Preset Name
channel-type.emotiva.speaker-preset.state.option.preset-1 = Speaker Preset 1
channel-type.emotiva.speaker-preset.state.option.preset-2 = Speaker Preset 2
channel-type.emotiva.source.label = Input Source
channel-type.emotiva.source.description = Select the input source for this zone of the device
channel-type.emotiva.standby.label = Standby
channel-type.emotiva.standby.description = Set device in standby mode
channel-type.emotiva.tuner-band.label = Radio Tuner Band
channel-type.emotiva.tuner-band.description = Set radio tuner band, "AM" or "FM"
channel-type.emotiva.tuner-band.state.option.band-am = AM
channel-type.emotiva.tuner-band.state.option.band-fm = FM
channel-type.emotiva.tuner-channel.label = Radio Tuner Channel Frequency
channel-type.emotiva.tuner-channel.description = Frequency of user selected radio channel
channel-type.emotiva.tuner-channel-select.label = Radio Tuner Channel Name
channel-type.emotiva.tuner-channel-select.description = Name of user selected radio channel
channel-type.emotiva.tuner-channel-select.state.option.channel-1 = Channel 1
channel-type.emotiva.tuner-channel-select.state.option.channel-2 = Channel 2
channel-type.emotiva.tuner-channel-select.state.option.channel-3 = Channel 3
channel-type.emotiva.tuner-channel-select.state.option.channel-4 = Channel 4
channel-type.emotiva.tuner-channel-select.state.option.channel-5 = Channel 5
channel-type.emotiva.tuner-channel-select.state.option.channel-6 = Channel 6
channel-type.emotiva.tuner-channel-select.state.option.channel-7 = Channel 7
channel-type.emotiva.tuner-channel-select.state.option.channel-8 = Channel 8
channel-type.emotiva.tuner-channel-select.state.option.channel-9 = Channel 9
channel-type.emotiva.tuner-channel-select.state.option.channel-10 = Channel 10
channel-type.emotiva.tuner-channel-select.state.option.channel-11 = Channel 11
channel-type.emotiva.tuner-channel-select.state.option.channel-12 = Channel 12
channel-type.emotiva.tuner-channel-select.state.option.channel-13 = Channel 13
channel-type.emotiva.tuner-channel-select.state.option.channel-14 = Channel 14
channel-type.emotiva.tuner-channel-select.state.option.channel-15 = Channel 15
channel-type.emotiva.tuner-channel-select.state.option.channel-16 = Channel 16
channel-type.emotiva.tuner-channel-select.state.option.channel-17 = Channel 17
channel-type.emotiva.tuner-channel-select.state.option.channel-18 = Channel 18
channel-type.emotiva.tuner-channel-select.state.option.channel-19 = Channel 19
channel-type.emotiva.tuner-channel-select.state.option.channel-20 = Channel 20
channel-type.emotiva.tuner-program.label = Radio Tuner Program
channel-type.emotiva.tuner-program.description = Radio tuner program: "Country", "Rock", "Classical", etc.
channel-type.emotiva.tuner-program.state.option.adult-hits = Adult Hits
channel-type.emotiva.tuner-program.state.option.alarm = Alarm
channel-type.emotiva.tuner-program.state.option.alarm-test = Alarm Test
channel-type.emotiva.tuner-program.state.option.children-programmes = Children's Programmes
channel-type.emotiva.tuner-program.state.option.classic-rock = Classic Rock
channel-type.emotiva.tuner-program.state.option.classical = Classical
channel-type.emotiva.tuner-program.state.option.college = College
channel-type.emotiva.tuner-program.state.option.country-music = Country Music
channel-type.emotiva.tuner-program.state.option.culture = Culture
channel-type.emotiva.tuner-program.state.option.current-affairs = Current Affairs
channel-type.emotiva.tuner-program.state.option.documentary = Documentary
channel-type.emotiva.tuner-program.state.option.drama = Drama
channel-type.emotiva.tuner-program.state.option.easy-listening = Easy Listening
channel-type.emotiva.tuner-program.state.option.education = Education
channel-type.emotiva.tuner-program.state.option.emergency = Emergency
channel-type.emotiva.tuner-program.state.option.emergency-test = Emergency Test
channel-type.emotiva.tuner-program.state.option.finance = Finance
channel-type.emotiva.tuner-program.state.option.folk-music = Folk Music
channel-type.emotiva.tuner-program.state.option.information = Information
channel-type.emotiva.tuner-program.state.option.jazz = Jazz
channel-type.emotiva.tuner-program.state.option.jazz-music = Jazz Music
channel-type.emotiva.tuner-program.state.option.language = Language
channel-type.emotiva.tuner-program.state.option.leisure = Leisure
channel-type.emotiva.tuner-program.state.option.light-classical = Light Classical
channel-type.emotiva.tuner-program.state.option.national-music = National Music
channel-type.emotiva.tuner-program.state.option.news = News
channel-type.emotiva.tuner-program.state.option.nostalgia = Nostalgia
channel-type.emotiva.tuner-program.state.option.oldies = Oldies (Music)
channel-type.emotiva.tuner-program.state.option.oldies-music = Oldies Music
channel-type.emotiva.tuner-program.state.option.other-music = Other Music
channel-type.emotiva.tuner-program.state.option.personality = Personality
channel-type.emotiva.tuner-program.state.option.phone-in = Phone-in
channel-type.emotiva.tuner-program.state.option.popular-music = Popular Music (Pop)
channel-type.emotiva.tuner-program.state.option.public = Public
channel-type.emotiva.tuner-program.state.option.religion = Religion
channel-type.emotiva.tuner-program.state.option.religious-talk = Religious Talk
channel-type.emotiva.tuner-program.state.option.rhythm-blues = Rhythm & Blues
channel-type.emotiva.tuner-program.state.option.rock = Rock
channel-type.emotiva.tuner-program.state.option.rock-music = Rock Music
channel-type.emotiva.tuner-program.state.option.science = Science
channel-type.emotiva.tuner-program.state.option.serious-classical = Serious Classical
channel-type.emotiva.tuner-program.state.option.social-affairs = Social Affairs
channel-type.emotiva.tuner-program.state.option.soft-music = Soft Music
channel-type.emotiva.tuner-program.state.option.soft-rhythm-blues = Soft Rhythm & Blues
channel-type.emotiva.tuner-program.state.option.soft-rock = Soft Rock
channel-type.emotiva.tuner-program.state.option.sport = Sport
channel-type.emotiva.tuner-program.state.option.talk = Talk
channel-type.emotiva.tuner-program.state.option.top-40 = Top 40
channel-type.emotiva.tuner-program.state.option.travel = Travel
channel-type.emotiva.tuner-program.state.option.weather = Weather
channel-type.emotiva.tuner-rds.label = Radio Tuner RDS
channel-type.emotiva.tuner-rds.description = Message from Radio Data System (RDS) for selected channel
channel-type.emotiva.tuner-signal.label = Radio Tuner Signal
channel-type.emotiva.tuner-signal.description = Radio tuner signal quality
channel-type.emotiva.video-format.label = Video Input Format
channel-type.emotiva.video-format.description = Current video input format: "1920x1080i/60", "3840x2160p/60", etc.
channel-type.emotiva.video-input.label = Video Input
channel-type.emotiva.video-input.description = Source for video input
channel-type.emotiva.video-space.label = Video Input Space
channel-type.emotiva.video-space.description = Current video input space: "YcbCr 8bits", etc.
channel-type.emotiva.volume.label = Volume
channel-type.emotiva.volume.description = Set the volume level of this zone
channel-type.emotiva.volume-db.label = Volume (dB)
channel-type.emotiva.volume-db.description = Set the volume level (dB).
channel-type.emotiva.volume-speaker-db.label = Speaker Trim
channel-type.emotiva.volume-speaker-db.description = Increased/Reduced volume for the speaker, treble or bass, in +/-dB
channel-type.emotiva.zonePower.label = Power (zone)
channel-type.emotiva.zonePower.description = Power ON/OFF this zone of the unit
# User Messages
message.processor.connecting = Connecting
message.processor.connection.failed = Failed to connect, check network connectivity and configuration
message.processor.connection.error.keep-alive = Failed to receive keepAlive message from device, check network connectivity!
message.processor.connection.error.port = portNumber is invalid!
message.processor.connection.error.address-empty = IP Address must be configured!
message.processor.connection.error.address-invalid = IP Address is not valid!
message.processor.notfound = Could not find device with ipAddress {0}
message.processor.goodbye = Device was shutdown

View File

@ -0,0 +1,532 @@
<?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>

View File

@ -0,0 +1,311 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils;
/**
* Abstract helper class for unit tests.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class AbstractDTOTestBase {
protected EmotivaXmlUtils xmlUtils = new EmotivaXmlUtils();
protected String emotivaAckPowerOff = """
<?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 {
}
}

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MAIN_VOLUME;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_MUTE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_STANDBY;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_SURROUND;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumeDecibelToPercentage;
import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.volumePercentageToDecibel;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.mute;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.mute_off;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.mute_on;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.standby;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.surround;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.surround_trim_set;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.volume;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.DIMENSIONLESS_DECIBEL;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.ON_OFF;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V2;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V3;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlRequest;
import org.openhab.binding.emotiva.internal.protocol.EmotivaDataType;
import org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion;
import org.openhab.core.library.types.PercentType;
/**
* Unit tests for the EmotivaCommandHelper.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaCommandHelperTest {
@Test
void volumeToPercentage() {
assertThat(volumeDecibelToPercentage("-100 dB"), is(PercentType.valueOf("0")));
assertThat(volumeDecibelToPercentage(" -96"), is(PercentType.valueOf("0")));
assertThat(volumeDecibelToPercentage("-41 dB "), is(PercentType.valueOf("50")));
assertThat(volumeDecibelToPercentage("15"), is(PercentType.valueOf("100")));
assertThat(volumeDecibelToPercentage("20"), is(PercentType.valueOf("100")));
}
@Test
void volumeToDecibel() {
assertThat(volumePercentageToDecibel("-10"), is(-96));
assertThat(volumePercentageToDecibel("0%"), is(-96));
assertThat(volumePercentageToDecibel("50 %"), is(-41));
assertThat(volumePercentageToDecibel("100 % "), is(15));
assertThat(volumePercentageToDecibel("110"), is(15));
}
private static Stream<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));
}
}

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
/**
* Unit tests for EmotivaAck message type.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaAckDTOTest extends AbstractDTOTestBase {
public EmotivaAckDTOTest() throws JAXBException {
}
@Test
void unmarshallValidCommand() throws JAXBException {
EmotivaAckDTO dto = (EmotivaAckDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaAckPowerOff);
assertThat(dto, is(notNullValue()));
assertThat(dto.getCommands().size(), is(1));
}
@Test
void unmarshallOneValidCommand() throws JAXBException {
EmotivaAckDTO dto = (EmotivaAckDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaAckPowerOffAndNotRealCommand);
assertThat(dto, is(notNullValue()));
List<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()));
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
/**
* Unit tests for EmotivaBarNotify message type.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaBarNotifyDTOTest extends AbstractDTOTestBase {
public EmotivaBarNotifyDTOTest() throws JAXBException {
}
@Test
void testUnmarshall() throws JAXBException {
EmotivaBarNotifyWrapper dto = (EmotivaBarNotifyWrapper) xmlUtils
.unmarshallToEmotivaDTO(emotivaBarNotifyBigText);
assertThat(dto.getSequence(), is("98"));
assertThat(dto.getTags().size(), is(1));
List<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()));
}
}

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
/**
* Unit tests for EmotivaCommandDTO command types.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaCommandDTOTest extends AbstractDTOTestBase {
public EmotivaCommandDTOTest() throws JAXBException {
}
@Test
void unmarshallElements() {
List<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()));
}
}

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Collections;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
/**
* Unit tests for EmotivaControl message type.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaControlDTOTest extends AbstractDTOTestBase {
public EmotivaControlDTOTest() throws JAXBException {
}
@Test
void marshalWithNoCommand() {
EmotivaControlDTO control = new EmotivaControlDTO(null);
String xmlString = xmlUtils.marshallJAXBElementObjects(control);
assertThat(xmlString, containsString("<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"));
}
}

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
/**
* Unit tests for EmotivaMenuNotify message type.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaMenuNotifyDTOTest extends AbstractDTOTestBase {
public EmotivaMenuNotifyDTOTest() throws JAXBException {
}
@Test
void testUnmarshallMenu() throws JAXBException {
EmotivaMenuNotifyDTO dto = (EmotivaMenuNotifyDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaMenuNotify);
assertThat(dto.getProgress(), is(nullValue()));
assertThat(dto.getSequence(), is("2378"));
assertThat(dto.getRow().size(), is(11));
assertThat(dto.getRow().size(), is(11));
assertThat(dto.getRow().get(0).getNumber(), is("0"));
assertThat(dto.getRow().get(0).getCol().size(), is(3));
assertThat(dto.getRow().get(0).getCol().get(0).getNumber(), is("0"));
assertThat(dto.getRow().get(0).getCol().get(0).getValue(), is(""));
assertThat(dto.getRow().get(0).getCol().get(0).getHighlight(), is("no"));
assertThat(dto.getRow().get(0).getCol().get(0).getArrow(), is("no"));
assertThat(dto.getRow().get(0).getCol().get(1).getNumber(), is("1"));
assertThat(dto.getRow().get(0).getCol().get(1).getValue(), is("Left Display"));
assertThat(dto.getRow().get(0).getCol().get(1).getHighlight(), is("no"));
assertThat(dto.getRow().get(0).getCol().get(1).getArrow(), is("up"));
assertThat(dto.getRow().get(0).getCol().get(2).getNumber(), is("2"));
assertThat(dto.getRow().get(0).getCol().get(2).getValue(), is("Full Status"));
assertThat(dto.getRow().get(0).getCol().get(2).getHighlight(), is("no"));
assertThat(dto.getRow().get(0).getCol().get(2).getArrow(), is("no"));
}
@Test
void testUnmarshallProgress() throws JAXBException {
EmotivaMenuNotifyDTO dto = (EmotivaMenuNotifyDTO) xmlUtils.unmarshallToEmotivaDTO(emotivaMenuNotifyProgress);
assertThat(dto.getSequence(), is("2405"));
assertThat(dto.getRow(), is(nullValue()));
assertThat(dto.getProgress().getTime(), is("15"));
}
}

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Collections;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
import org.w3c.dom.Element;
/**
* Unit tests for EmotivaNotify wrapper.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaNotifyWrapperTest extends AbstractDTOTestBase {
public EmotivaNotifyWrapperTest() throws JAXBException {
}
@Test
void marshallWithNoProperty() {
EmotivaNotifyWrapper dto = new EmotivaNotifyWrapper(emotivaNotifyV2KeepAliveSequence, Collections.emptyList());
String xmlAsString = xmlUtils.marshallEmotivaDTO(dto);
assertThat(xmlAsString,
containsString("<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()));
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
/**
* Unit tests for EmotivaPing message type.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaPingDTOTest extends AbstractDTOTestBase {
public EmotivaPingDTOTest() throws JAXBException {
}
@Test
void marshallPlain() {
EmotivaPingDTO dto = new EmotivaPingDTO();
String xmlAsString = xmlUtils.marshallEmotivaDTO(dto);
assertThat(xmlAsString, containsString("<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"));
}
}

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus.VALID;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* Unit tests for EmotivaCommandDTO command types.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaPropertyDTOTest extends AbstractDTOTestBase {
public EmotivaPropertyDTOTest() throws JAXBException {
}
@Test
void unmarshallFromEmotivaNotify() throws JAXBException {
EmotivaPropertyDTO commandDTO = (EmotivaPropertyDTO) xmlUtils
.unmarshallToEmotivaDTO(emotivaNotifyEmotivaPropertyPower);
assertThat(commandDTO, is(notNullValue()));
assertThat(commandDTO.getName(), is(EmotivaSubscriptionTags.tuner_channel.name()));
assertThat(commandDTO.getValue(), is("FM 106.50MHz"));
assertThat(commandDTO.getVisible(), is("true"));
assertThat(commandDTO.getStatus(), is(notNullValue()));
}
@Test
void unmarshallFromEmotivaUpdate() throws JAXBException {
EmotivaPropertyDTO commandDTO = (EmotivaPropertyDTO) xmlUtils
.unmarshallToEmotivaDTO(emotivaUpdateEmotivaPropertyPower);
assertThat(commandDTO, is(notNullValue()));
assertThat(commandDTO.getName(), is(EmotivaControlCommands.power.name()));
assertThat(commandDTO.getValue(), is("On"));
assertThat(commandDTO.getVisible(), is("true"));
assertThat(commandDTO.getStatus(), is(VALID.getValue()));
}
}

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_RDS;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.PROTOCOL_V2;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* Unit tests for EmotivaSubscription requests.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaSubscriptionRequestTest extends AbstractDTOTestBase {
public EmotivaSubscriptionRequestTest() throws JAXBException {
}
@Test
void marshalFromChannelUID() {
EmotivaSubscriptionTags subscriptionChannel = EmotivaSubscriptionTags.fromChannelUID(CHANNEL_TUNER_RDS);
EmotivaSubscriptionRequest emotivaSubscriptionRequest = new EmotivaSubscriptionRequest(subscriptionChannel);
String xmlString = xmlUtils.marshallJAXBElementObjects(emotivaSubscriptionRequest);
assertThat(xmlString, containsString("<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()));
}
}

View File

@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.power_on;
import java.util.Collections;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* Unit tests for EmotivaSubscription responses.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaSubscriptionResponseTest extends AbstractDTOTestBase {
public EmotivaSubscriptionResponseTest() throws JAXBException {
}
@Test
void marshallNoProperty() {
var dto = new EmotivaSubscriptionResponse(Collections.emptyList());
String xmlString = xmlUtils.marshallEmotivaDTO(dto);
assertThat(xmlString, containsString("<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"));
}
}

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
/**
* Unit tests for EmotivaTransponder message type.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaTransponderDTOTest extends AbstractDTOTestBase {
public EmotivaTransponderDTOTest() throws JAXBException {
}
@Test
void unmarshallV2() throws JAXBException {
EmotivaTransponderDTO dto = (EmotivaTransponderDTO) xmlUtils
.unmarshallToEmotivaDTO(emotivaTransponderResponseV2);
assertThat(dto, is(notNullValue()));
assertThat(dto.getModel(), is("XMC-1"));
assertThat(dto.getRevision(), is("2.0"));
assertThat(dto.getName(), is("Living Room"));
assertThat(dto.getControl().getVersion(), is("2.0"));
assertThat(dto.getControl().getControlPort(), is(7002));
assertThat(dto.getControl().getNotifyPort(), is(7003));
assertThat(dto.getControl().getInfoPort(), is(7004));
assertThat(dto.getControl().getSetupPortTCP(), is(7100));
assertThat(dto.getControl().getKeepAlive(), is(10000));
}
@Test
void unmarshallV3() throws JAXBException {
EmotivaTransponderDTO dto = (EmotivaTransponderDTO) xmlUtils
.unmarshallToEmotivaDTO(emotivaTransponderResponseV3);
assertThat(dto, is(notNullValue()));
assertThat(dto.getModel(), is("XMC-2"));
assertThat(dto.getRevision(), is("3.0"));
assertThat(dto.getName(), is("Living Room"));
assertThat(dto.getControl().getVersion(), is("3.0"));
assertThat(dto.getControl().getControlPort(), is(7002));
assertThat(dto.getControl().getNotifyPort(), is(7003));
assertThat(dto.getControl().getInfoPort(), is(7004));
assertThat(dto.getControl().getSetupPortTCP(), is(7100));
assertThat(dto.getControl().getKeepAlive(), is(10000));
}
}

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_RDS;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* Unit tests for EmotivaUnsubscribe requests.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaUnsubscriptionTest extends AbstractDTOTestBase {
public EmotivaUnsubscriptionTest() throws JAXBException {
}
@Test
void marshalFromChannelUID() {
EmotivaSubscriptionTags subscriptionChannel = EmotivaSubscriptionTags.fromChannelUID(CHANNEL_TUNER_RDS);
EmotivaUnsubscribeDTO emotivaSubscriptionRequest = new EmotivaUnsubscribeDTO(subscriptionChannel);
String xmlString = xmlUtils.marshallJAXBElementObjects(emotivaSubscriptionRequest);
assertThat(xmlString, containsString("<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>")));
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Collections;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.EmotivaBindingConstants;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* Unit tests for EmotivaUpdate requests.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaUpdateRequestTest extends AbstractDTOTestBase {
public EmotivaUpdateRequestTest() throws JAXBException {
}
@Test
void marshallWithNoProperty() {
EmotivaUpdateRequest dto = new EmotivaUpdateRequest(Collections.emptyList());
String xmlAsString = xmlUtils.marshallJAXBElementObjects(dto);
assertThat(xmlAsString, containsString("<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>"));
}
}

View File

@ -0,0 +1,97 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.dto;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus.NOT_VALID;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaPropertyStatus.VALID;
import java.util.Collections;
import java.util.List;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
/**
* Unit tests for EmotivaUpdate responses.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaUpdateResponseTest extends AbstractDTOTestBase {
public EmotivaUpdateResponseTest() throws JAXBException {
}
@Test
void marshallWithNoProperty() {
EmotivaUpdateResponse dto = new EmotivaUpdateResponse(Collections.emptyList());
String xmlAsString = xmlUtils.marshallEmotivaDTO(dto);
assertThat(xmlAsString, containsString("<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()));
}
}

View File

@ -0,0 +1,270 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.*;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.*;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.*;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_band;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.tuner_channel;
import static org.openhab.core.types.RefreshType.REFRESH;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.binding.emotiva.internal.EmotivaBindingConstants;
import org.openhab.binding.emotiva.internal.EmotivaCommandHelper;
import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* Unit tests for EmotivaControl requests.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaControlRequestTest {
private static Stream<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));
}
}

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.emotiva.internal.protocol;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import javax.xml.bind.JAXBException;
import javax.xml.bind.UnmarshalException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.AbstractDTOTestBase;
import org.openhab.binding.emotiva.internal.dto.EmotivaNotifyWrapper;
/**
* Unit tests for Emotiva message marshalling and unmarshalling.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaXmlUtilsTest extends AbstractDTOTestBase {
public EmotivaXmlUtilsTest() throws JAXBException {
}
@Test
void testUnmarshallEmptyString() {
assertThrows(JAXBException.class, () -> xmlUtils.unmarshallToEmotivaDTO(""), "xml value is null or empty");
}
@Test
void testUnmarshallNotValidXML() {
assertThrows(UnmarshalException.class, () -> xmlUtils.unmarshallToEmotivaDTO("notXmlAtAll"));
}
@Test
void testUnmarshallInstanceObject() throws JAXBException {
Object object = xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2KeepAlive);
assertThat(object, instanceOf(EmotivaNotifyWrapper.class));
}
@Test
void testUnmarshallXml() throws JAXBException {
Object object = xmlUtils.unmarshallToEmotivaDTO(emotivaNotifyV2KeepAlive);
assertThat(object, instanceOf(EmotivaNotifyWrapper.class));
}
@Test
void testMarshallObjectWithoutXmlElements() {
String commands = xmlUtils.marshallEmotivaDTO("");
assertThat(commands, is(""));
}
@Test
void testMarshallNoValueDTO() {
EmotivaNotifyWrapper dto = new EmotivaNotifyWrapper();
String xmlAsString = xmlUtils.marshallEmotivaDTO(dto);
assertThat(xmlAsString, not(containsString("<emotivaNotify>")));
assertThat(xmlAsString, containsString("<emotivaNotify/>"));
}
}

View File

@ -129,6 +129,7 @@
<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>