[emotiva] Tag group handler and bugfixes. (#18437)

* Adds tag group subscription handler to reduce load on processor devices.
* Fixes matching of input label to Emotiva source command.
* Fixes cleaning of source input to work via REST calls.
* Fixes updating of custom user labels on binding startup.
* Fixes issue with missing event publisher in EmotivaInputStateOptionProvider.

Signed-off-by: Espen Fossen <espenaf@junta.no>
This commit is contained in:
Espen Fossen
2025-03-30 08:03:24 +02:00
committed by GitHub
parent dae03087a7
commit 6c4b3c7186
18 changed files with 953 additions and 199 deletions
+22 -11
View File
@@ -18,17 +18,28 @@ The binding automatically discovers devices on your network.
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 |
| 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 |
| activateFrontBar | Activates Front Bar channels | false |
| activateOSDMenu | Activates OSD menu channels | false |
| activateZone2 | Activates Zone 2 channels | false |
| 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 |
### Dynamic channels and extended functionality
Emotiva processors have limited processing power, so if the binding subscribes to all channels simultaneously the
device might grind to a halt after a while, requiring a manual reboot of the device. The binding is designed to
dynamically enable and disabled channels based on the selected source. Furthermore, the configuration values
**activateFrontBar**, **activateOSDMenu** and **activateZone2** controls extra functionality this is default off to
reduce the overall load on the device.
## Channels
@@ -47,6 +47,12 @@ public class EmotivaBindingConstants {
/** Connection retry interval in minutes */
static final int DEFAULT_RETRY_INTERVAL_MINUTES = 2;
/** Refresh source labels interval in minutes */
static final int DEFAULT_REFRESH_SOURCE_LABEL_JOB_IN_MINUTES = 60;
/** Refresh source labels unsubscribe delay, giving enough time to get answer from device */
static final int DEFAULT_REFRESH_SOURCE_UNSUBSCRIBE_DELAY_IN_SECONDS = 60;
/**
* Default Emotiva device keep alive in milliseconds. {@link org.openhab.binding.emotiva.internal.dto.ControlDTO}
*/
@@ -94,6 +100,11 @@ public class EmotivaBindingConstants {
static final String MENU_PANEL_HIGHLIGHTED = "true";
static final String EMOTIVA_SOURCE_COMMAND_PREFIX = "source_";
/** Emotiva Configuration Constants **/
public static final String CONFIG_ACTIVATE_FRONT_BAR = "activateFrontBar";
public static final String CONFIG_ACTIVATE_OSD_MENU = "activateOSDMenu";
public static final String CONFIG_ACTIVATE_ZONE2 = "activateZone2";
/** Emotiva Protocol V1 channels **/
public static final String CHANNEL_STANDBY = "general#standby";
public static final String CHANNEL_MAIN_ZONE_POWER = "main-zone#power";
@@ -30,6 +30,9 @@ public class EmotivaConfiguration {
public int infoPort = 7004;
public int setupPortTCP = 7100;
public int menuNotifyPort = 7005;
public boolean activateFrontBar = false;
public boolean activateOSDMenu = false;
public boolean activateZone2 = false;
public String protocolVersion = DEFAULT_EMOTIVA_PROTOCOL_VERSION;
public int keepAlive = DEFAULT_KEEP_ALIVE_IN_MILLISECONDS;
public int retryConnectInMinutes = DEFAULT_RETRY_INTERVAL_MINUTES;
@@ -28,13 +28,20 @@ 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.events.EventPublisher;
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.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption;
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;
@@ -44,13 +51,24 @@ import org.slf4j.LoggerFactory;
* @author Kai Kreuzer - Initial contribution
* @author Espen Fossen - Adapted to Emotiva binding
*/
@Component(service = { DynamicStateDescriptionProvider.class, EmotivaInputStateOptionProvider.class })
@NonNullByDefault
public class InputStateOptionProvider extends BaseDynamicStateDescriptionProvider implements ThingHandlerService {
public class EmotivaInputStateOptionProvider extends BaseDynamicStateDescriptionProvider
implements ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(InputStateOptionProvider.class);
private final Logger logger = LoggerFactory.getLogger(EmotivaInputStateOptionProvider.class);
private @Nullable EmotivaProcessorHandler handler;
@Activate
public EmotivaInputStateOptionProvider(final @Reference EventPublisher eventPublisher,
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = (EmotivaProcessorHandler) handler;
@@ -73,9 +91,9 @@ public class InputStateOptionProvider extends BaseDynamicStateDescriptionProvide
EmotivaProcessorHandler localHandler = handler;
if (localHandler != null) {
if (channel.getUID().getId().equals(CHANNEL_SOURCE)) {
setStateOptionsForSource(channel, options, localHandler.getSourcesMainZone());
options.addAll(gatherStateOptionsForSource(channel, localHandler.getSourcesMainZone()));
} else if (channel.getUID().getId().equals(CHANNEL_ZONE2_SOURCE)) {
setStateOptionsForSource(channel, options, localHandler.getSourcesZone2());
options.addAll(gatherStateOptionsForSource(channel, localHandler.getSourcesZone2()));
} else if (channel.getUID().getId().equals(CHANNEL_MODE)) {
EnumMap<EmotivaSubscriptionTags, String> modes = localHandler.getModes();
Collection<EmotivaSubscriptionTags> modeKeys = modes.keySet();
@@ -83,24 +101,26 @@ public class InputStateOptionProvider extends BaseDynamicStateDescriptionProvide
options.add(new StateOption(modeKey.name(), modes.get(modeKey)));
}
logger.trace("Updating OH channel '{}' with state options '{}'", CHANNEL_MODE, options);
setStateOptions(channel.getUID(), 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)));
List<StateOption> gatherStateOptionsForSource(Channel channel,
EnumMap<EmotivaControlCommands, String> sourcesWithUserLabels) {
List<StateOption> options = new ArrayList<>();
Collection<EmotivaControlCommands> sourceCommands = sourcesWithUserLabels.keySet();
for (EmotivaControlCommands command : sourceCommands) {
// If command name starts with prefix "source_", add the user provided label, else use default label from
// command.
if (command.name().startsWith(EMOTIVA_SOURCE_COMMAND_PREFIX)) {
options.add(new StateOption(command.name(), sourcesWithUserLabels.get(command)));
} else {
options.add(new StateOption(sourceKey.name(), sourceKey.getLabel()));
options.add(new StateOption(command.name(), command.getLabel()));
}
}
logger.trace("Updating OH channel '{}' with state options '{}'", channel.getUID().getId(), options);
setStateOptions(channel.getUID(), options);
return options;
}
}
@@ -25,8 +25,8 @@ import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlComman
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.EmotivaSubscriptionTagGroup.SOURCES;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.keepAlive;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.noSubscriptionToChannel;
import java.io.IOException;
import java.io.InterruptedIOException;
@@ -39,11 +39,15 @@ import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import javax.measure.quantity.Frequency;
import javax.xml.bind.JAXBException;
@@ -67,6 +71,7 @@ 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.EmotivaSubscriptionTagGroup;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
import org.openhab.binding.emotiva.internal.protocol.EmotivaUdpResponse;
import org.openhab.binding.emotiva.internal.protocol.EmotivaXmlUtils;
@@ -112,10 +117,12 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
private final List<EmotivaSubscriptionTags> zone2Subscriptions = EmotivaSubscriptionTags.channels("zone2");
private final EmotivaProcessorState state = new EmotivaProcessorState();
private final EmotivaSubscriptionTagGroupHandler subscriptionHandler;
private final EmotivaTranslationProvider i18nProvider;
private @Nullable ScheduledFuture<?> pollingJob;
private @Nullable ScheduledFuture<?> connectRetryJob;
private @Nullable ScheduledFuture<?> sourceLabelRefreshJob;
private @Nullable EmotivaUdpSendingService sendingService;
private @Nullable EmotivaUdpReceivingService notifyListener;
private @Nullable EmotivaUdpReceivingService menuNotifyListener;
@@ -135,6 +142,7 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
super(thing);
this.i18nProvider = i18nProvider;
this.config = getConfigAs(EmotivaConfiguration.class);
this.subscriptionHandler = new EmotivaSubscriptionTagGroupHandler(config, state);
this.retryConnectInMinutes = config.retryConnectInMinutes;
}
@@ -180,9 +188,7 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
for (int attempt = 1; attempt <= DEFAULT_CONNECTION_RETRIES && !udpSenderActive; attempt++) {
try {
logger.debug("Connection attempt '{}'", attempt);
sendConnector.sendSubscription(generalSubscription, config);
sendConnector.sendSubscription(mainZoneSubscriptions, config);
sendConnector.sendSubscription(zone2Subscriptions, config);
sendConnector.sendSubscription(subscriptionHandler.init(), config);
} catch (IOException e) {
// network or socket failure, also wait 2 sec and try again
}
@@ -195,6 +201,8 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
if (udpSenderActive) {
updateStatus(ThingStatus.ONLINE);
state.updateLastSeen(ZonedDateTime.now(ZoneId.systemDefault()).toInstant());
setInitialSourceLabels();
starthSourceLabelsRefresJob();
startPollingKeepAlive();
final var menuListenerConnector = new EmotivaUdpReceivingService(localConfig.menuNotifyPort,
@@ -220,7 +228,10 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
private void scheduleConnectRetry(long waitMinutes) {
logger.debug("Scheduling connection retry in '{}' minutes", waitMinutes);
connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
final ScheduledFuture<?> localScheduleConnectRetryJob = connectRetryJob;
if (localScheduleConnectRetryJob == null || localScheduleConnectRetryJob.isCancelled()) {
connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
}
}
/**
@@ -245,6 +256,82 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
}
}
/**
* Starts a polling job for refreshing source labels
*/
private void starthSourceLabelsRefresJob() {
final ScheduledFuture<?> localSourceLabelRefreshJob = sourceLabelRefreshJob;
if (localSourceLabelRefreshJob == null || localSourceLabelRefreshJob.isCancelled()) {
sourceLabelRefreshJob = scheduler.scheduleWithFixedDelay(this::refreshSourceLabels, 0,
DEFAULT_REFRESH_SOURCE_LABEL_JOB_IN_MINUTES, TimeUnit.MINUTES);
logger.debug("Started scheduled job to update source labels now and every {}min",
DEFAULT_REFRESH_SOURCE_LABEL_JOB_IN_MINUTES);
}
}
private void refreshSourceLabels() {
EmotivaUdpSendingService localSendingService = sendingService;
if (localSendingService != null) {
try {
localSendingService
.sendSubscription(EmotivaSubscriptionTags.getBySubscriptionTagGroups(Set.of(SOURCES)), config);
scheduler.schedule(this::unsubscribeSourceLabels, DEFAULT_REFRESH_SOURCE_UNSUBSCRIBE_DELAY_IN_SECONDS,
TimeUnit.SECONDS);
} catch (InterruptedIOException e) {
logger.debug("Interrupted during sending of EmotivaSubscription message to device '{}'", thing.getUID(),
e);
} catch (IOException e) {
logger.warn("Failed to send EmotivaSubscription message to device '{}'", thing.getUID(), e);
}
}
}
private void unsubscribeSourceLabels() {
EmotivaUdpSendingService localSendingService = sendingService;
if (localSendingService != null) {
try {
localSendingService
.sendUnsubscribe(EmotivaSubscriptionTags.getBySubscriptionTagGroups(Set.of(SOURCES)));
} catch (InterruptedIOException e) {
logger.debug("Interrupted during sending of EmotivaUnsubscribe message to device '{}'", thing.getUID(),
e);
} catch (IOException e) {
logger.warn("Failed to send EmotivaUnsubscribe message to device '{}'", thing.getUID(), e);
}
}
}
private void subscribeTagGroups(Set<EmotivaSubscriptionTagGroup> groups) {
EmotivaUdpSendingService localSendingService = sendingService;
if (localSendingService != null) {
try {
localSendingService.sendSubscription(EmotivaSubscriptionTags.getBySubscriptionTagGroups(groups),
config);
state.updateSubscribedTagGroups(groups);
} catch (InterruptedIOException e) {
logger.debug("Interrupted during sending of EmotivaSubscription message to device '{}'", thing.getUID(),
e);
} catch (IOException e) {
logger.warn("Failed to send EmotivaSubscription message to device '{}'", thing.getUID(), e);
}
}
}
private void unsubscribeTagGroups(Set<EmotivaSubscriptionTagGroup> groups) {
EmotivaUdpSendingService localSendingService = sendingService;
if (localSendingService != null) {
try {
localSendingService.sendUnsubscribe(EmotivaSubscriptionTags.getBySubscriptionTagGroups(groups));
state.updateUnsubscribedTagGroups(groups);
} catch (InterruptedIOException e) {
logger.debug("Interrupted during sending of EmotivaUnsubscribe message to device '{}'", thing.getUID(),
e);
} catch (IOException e) {
logger.warn("Failed to send EmotivaUnsubscribe message to device '{}'", thing.getUID(), e);
}
}
}
private void checkKeepAliveTimestamp() {
Instant lastKeepAliveTimestamp = state.getLastSeen();
@@ -355,17 +442,6 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
} else if (object instanceof EmotivaSubscriptionResponse answerDto) {
logger.trace("Processing received '{}' with '{}'", EmotivaSubscriptionResponse.class.getSimpleName(),
emotivaUdpResponse.answer());
// Populates static input sources, except input
EnumMap<EmotivaControlCommands, String> sourceMainZone = EmotivaControlCommands
.getCommandsFromType(EmotivaCommandType.SOURCE_MAIN_ZONE);
sourceMainZone.remove(EmotivaControlCommands.input);
state.setSourcesMainZone(sourceMainZone);
EnumMap<EmotivaControlCommands, String> sourcesZone2 = EmotivaControlCommands
.getCommandsFromType(EmotivaCommandType.SOURCE_ZONE2);
sourcesZone2.remove(EmotivaControlCommands.input);
state.setSourcesZone2(sourcesZone2);
if (answerDto.getProperties() == null) {
for (EmotivaNotifyDTO dto : xmlUtils.unmarshallToNotification(answerDto.getTags())) {
handleChannelUpdate(dto.getName(), dto.getValue(), dto.getVisible(), dto.getAck());
@@ -379,6 +455,21 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
}
}
/**
* Sets initial source labels based on command list.
*/
private void setInitialSourceLabels() {
EnumMap<EmotivaControlCommands, String> sourceMainZone = EmotivaControlCommands
.getByCommandType(EmotivaCommandType.SOURCE_MAIN_ZONE);
sourceMainZone.remove(EmotivaControlCommands.input);
state.setSourcesMainZone(sourceMainZone);
EnumMap<EmotivaControlCommands, String> sourcesZone2 = EmotivaControlCommands
.getByCommandType(EmotivaCommandType.SOURCE_ZONE2);
sourcesZone2.remove(EmotivaControlCommands.input);
state.setSourcesZone2(sourcesZone2);
}
private void handleMenuNotify(EmotivaMenuNotifyDTO answerDto) {
String highlightValue = "";
@@ -469,16 +560,16 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
}
}
private void handleChannelUpdate(String emotivaSubscriptionName, String value, String visible, String status) {
private void handleChannelUpdate(String emotivaSubscriptionName, String rawValue, String visible, String status) {
logger.trace("Subscription property '{}' with raw value '{}' received, start processing",
emotivaSubscriptionName, value);
emotivaSubscriptionName, rawValue);
if (status.equals(NOT_VALID.name())) {
logger.debug("Subscription property '{}' not present in device, skipping", emotivaSubscriptionName);
return;
}
if ("None".equals(value)) {
if ("None".equals(rawValue)) {
logger.debug(
"Subscription property '{}' has no value, no update needed, usually means a speaker is not enabled",
emotivaSubscriptionName);
@@ -489,7 +580,7 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
state.updateLastSeen(ZonedDateTime.now(ZoneId.systemDefault()).toInstant());
logger.trace(
"Subscription property '{}' with value '{}' mapped to last-seen for device '{}', value updated",
keepAlive.name(), value.trim(), thing.getUID());
keepAlive.name(), rawValue, thing.getUID());
return;
}
@@ -501,13 +592,6 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
return;
}
if (noSubscriptionToChannel().contains(EmotivaSubscriptionTags.valueOf(emotivaSubscriptionName))) {
logger.debug(
"Subscription property '{}' is not mapped to a OH channel, no update needed, only used for logging",
emotivaSubscriptionName);
return;
}
try {
EmotivaSubscriptionTags subscriptionTag;
try {
@@ -519,19 +603,20 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
}
if (subscriptionTag.getChannel().isEmpty()) {
logger.debug("Subscription property '{}' does not have a corresponding OH channel, skipping",
emotivaSubscriptionName);
logger.debug(
"Subscription property '{}' with value '{}' does not have a corresponding OH channel, skipping",
emotivaSubscriptionName, rawValue);
return;
}
String trimmedValue = value.trim();
String trimmedValue = rawValue.trim();
logger.trace("Subscription property '{}' with value '{}' mapped to OH channel '{}'", subscriptionTag,
trimmedValue, subscriptionTag.getChannel());
// Add/Update user assigned name for inputs
// Add/Update user assigned name for sources
if (subscriptionTag.getChannel().startsWith(CHANNEL_INPUT1.substring(0, CHANNEL_INPUT1.indexOf("-") + 1))
&& "true".equals(visible)) {
state.updateSourcesMainZone(EmotivaControlCommands.matchToInput(subscriptionTag.name()), trimmedValue);
state.updateSourcesMainZone(EmotivaControlCommands.matchFromSourceInput(subscriptionTag), trimmedValue);
logger.trace("Adding/Updating '{}' to OH channel '{}' state options, all options are now {}",
trimmedValue, CHANNEL_SOURCE, state.getSourcesMainZone());
}
@@ -621,6 +706,21 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
logger.debug("Preparing to update OH channel '{}' with value:type '{}:{}'", channelName, value,
StringType.class.getSimpleName());
updateChannelState(channelName, StringType.valueOf(value));
if (channelName.equals(CHANNEL_SOURCE)) {
EmotivaControlCommands matchedSource = matchCommandFromSourceAndLabels(value);
if (matchedSource.equals(none)) {
logger.error(
"Error trying to get source command from OhCommand '{}:{}', not able to update subscriptions",
channelName, value);
} else {
BiConsumer<Set<EmotivaSubscriptionTagGroup>, Set<EmotivaSubscriptionTagGroup>> tagGroups = (
subscribeSet, unsubscribeSet) -> {
updateTagGroupChangesForSource(subscribeSet, unsubscribeSet, value, matchedSource);
};
subscriptionHandler.tagGroupsFromSource(matchedSource, tagGroups);
logger.debug("Currently subscribed tag groups: '{}'", state.getSubscriptionsTagGroups());
}
}
}
case UNKNOWN -> // Do nothing, types not connect to channels
logger.debug("Channel '{}' with UNKNOWN type and value '{}' was not updated", channelName, value);
@@ -630,6 +730,43 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
}
}
/**
* Updates tags subscriptions by the binding based on a source command and tag groups. The subscribe and unsubscribe
* sets are matched against the currently subscribed tag groups to reduce number of needed subscription calls and
* avoid recursive updates.
*
* @param subscribeSet Tag groups to subscribe to
* @param unsubscribeSet Tag groups to unsubscribe from
* @param value Input value from channel er device
* @param matchedSource Source command matched from input value
*/
private void updateTagGroupChangesForSource(Set<EmotivaSubscriptionTagGroup> subscribeSet,
Set<EmotivaSubscriptionTagGroup> unsubscribeSet, String value, EmotivaControlCommands matchedSource) {
Set<EmotivaSubscriptionTagGroup> currentSubscriptionTagGroups = state.getSubscriptionsTagGroups();
var unsubscribe = unsubscribeSet.stream().filter(currentSubscriptionTagGroups::contains)
.collect(Collectors.toCollection(HashSet::new));
if (!unsubscribe.isEmpty()) {
unsubscribeTagGroups(unsubscribeSet);
}
var subscribe = subscribeSet.stream().filter(group -> !currentSubscriptionTagGroups.contains(group))
.collect(Collectors.toCollection(HashSet::new));
if (!subscribe.isEmpty()) {
subscribeTagGroups(subscribe);
}
logger.debug("Input '{}' matched to source '{}' subscribing to '{}' and unsubscribing from '{}'", value,
matchedSource.name(), subscribe, unsubscribe);
}
public EmotivaControlCommands matchCommandFromSourceAndLabels(String value) {
Map<EmotivaControlCommands, String> map = state.getCommandMap(MAP_SOURCES_MAIN_ZONE);
EmotivaControlCommands command = EmotivaControlCommands.matchFromSourceInput(value, map);
if (command.equals(none)) {
logger.debug("Could not match OH command {} with value '{}' to values in map '{}', no matching command",
CHANNEL_SOURCE, value, map);
}
return command;
}
private void updateChannelState(String channelID, State channelState) {
state.updateChannel(channelID, channelState);
logger.trace("Updating OH channel '{}' with value '{}'", channelID, channelState);
@@ -670,7 +807,26 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
.or(() -> Optional.of(emotivaRequest.createDTO(ohCommand, UnDefType.UNDEF)));
localSendingService.send(dto.get());
if (emotivaRequest.getName().equals(EmotivaControlCommands.volume.name())
if (emotivaRequest.getDefaultCommand().equals(EmotivaControlCommands.input)
&& ohCommand instanceof StringType sourceString) {
try {
EmotivaControlCommands source = EmotivaControlCommands.valueOf(sourceString.toString());
BiConsumer<Set<EmotivaSubscriptionTagGroup>, Set<EmotivaSubscriptionTagGroup>> tagGroups = (
subscribeSet, unsubscribeSet) -> {
logger.debug(
"OhCommand '{}:{}' is a source command with subscribeSet '{}' and unsubscribeSet '{}'",
channelUID.getId(), ohCommand, subscribeSet, unsubscribeSet);
updateTagGroupChangesForSource(subscribeSet, unsubscribeSet, sourceString.toString(),
source);
};
subscriptionHandler.tagGroupsFromSource(source, tagGroups);
logger.debug("Currently subscribed tag groups: '{}'", state.getSubscriptionsTagGroups());
} catch (IllegalArgumentException e) {
logger.error(
"Error trying to get source command from OhCommand '{}:{}', not able to update subscriptions",
channelUID.getId(), ohCommand);
}
} else if (emotivaRequest.getName().equals(EmotivaControlCommands.volume.name())
|| emotivaRequest.getName().equals(EmotivaControlCommands.set_volume.name())) {
logger.debug("OhCommand '{}:{}' is of type main zone volume", channelUID.getId(), ohCommand);
if (ohCommand instanceof PercentType value) {
@@ -776,7 +932,7 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(InputStateOptionProvider.class);
return Set.of(EmotivaInputStateOptionProvider.class);
}
public EnumMap<EmotivaControlCommands, String> getSourcesMainZone() {
@@ -24,11 +24,14 @@ import java.time.Instant;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTagGroup;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
import org.openhab.core.types.State;
@@ -44,6 +47,7 @@ public class EmotivaProcessorState {
private EnumMap<EmotivaControlCommands, String> sourcesMainZone;
private EnumMap<EmotivaControlCommands, String> sourcesZone2;
private final Set<EmotivaSubscriptionTagGroup> subscriptions = new HashSet<>();
private final EnumMap<EmotivaSubscriptionTags, String> modes;
private Instant lastSeen = Instant.EPOCH;
@@ -137,8 +141,8 @@ public class EmotivaProcessorState {
channelStateMap.put(channel, state);
}
public void updateSourcesMainZone(EmotivaControlCommands command, String value) {
sourcesMainZone.put(command, value);
public void updateSourcesMainZone(EmotivaControlCommands command, String label) {
sourcesMainZone.put(command, label.trim());
}
public void updateModes(EmotivaSubscriptionTags tag, String value) {
@@ -156,4 +160,16 @@ public class EmotivaProcessorState {
public Instant getLastSeen() {
return lastSeen;
}
public Set<EmotivaSubscriptionTagGroup> getSubscriptionsTagGroups() {
return subscriptions;
}
public void updateSubscribedTagGroups(Set<EmotivaSubscriptionTagGroup> toAddSubscriptions) {
subscriptions.addAll(toAddSubscriptions);
}
public void updateUnsubscribedTagGroups(Set<EmotivaSubscriptionTagGroup> toRemoveSubscriptions) {
subscriptions.removeAll(toRemoveSubscriptions);
}
}
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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.protocol.EmotivaSubscriptionTagGroup.UI_DEVICE;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTagGroup;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles subscription tags against av given Emotiva device. Devices have limited processing power, so the device do
* not expect an integration to subscribe to all possible tags at the same time. Tags are divided into groups that
* correspond with source/input the device is using or configuration values set in the binding.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class EmotivaSubscriptionTagGroupHandler {
private final Logger logger = LoggerFactory.getLogger(EmotivaSubscriptionTagGroupHandler.class);
private final EmotivaConfiguration config;
private final EmotivaProcessorState state;
private final List<EmotivaSubscriptionTagGroup> basicSubscriptions = List.of(EmotivaSubscriptionTagGroup.GENERAL,
EmotivaSubscriptionTagGroup.AUDIO_ADJUSTMENT);
public EmotivaSubscriptionTagGroupHandler(EmotivaConfiguration config, EmotivaProcessorState state) {
this.config = config;
this.state = state;
}
public List<EmotivaSubscriptionTags> init() {
Set<EmotivaSubscriptionTagGroup> unsubscribe = new HashSet<>();
Set<EmotivaSubscriptionTagGroup> subscribe = new HashSet<>(basicSubscriptions);
if (config.activateFrontBar) {
subscribe.add(UI_DEVICE);
} else {
unsubscribe.add(UI_DEVICE);
}
if (config.activateOSDMenu) {
subscribe.add(EmotivaSubscriptionTagGroup.UI_MENU);
} else {
unsubscribe.add(EmotivaSubscriptionTagGroup.UI_MENU);
}
if (config.activateZone2) {
subscribe.add(EmotivaSubscriptionTagGroup.ZONE2_GENERAL);
} else {
unsubscribe.add(EmotivaSubscriptionTagGroup.ZONE2_GENERAL);
}
state.updateUnsubscribedTagGroups(unsubscribe);
state.updateSubscribedTagGroups(subscribe);
Set<EmotivaSubscriptionTagGroup> tagGroups = state.getSubscriptionsTagGroups();
List<EmotivaSubscriptionTags> tags = EmotivaSubscriptionTags.getBySubscriptionTagGroups(tagGroups);
logger.trace("Subscribing to tag groups '{}' and tags '{}' on binding startup.", tagGroups, tags);
return tags;
}
/**
* Updates a BiConsumer with subscribe and unsubscribe tag groups based on provided source.
*
* @param source Provided source.
* @param tagGroups Consumer for subscribe and unsubscribe tag groups.
*/
public void tagGroupsFromSource(EmotivaControlCommands source,
BiConsumer<Set<EmotivaSubscriptionTagGroup>, Set<EmotivaSubscriptionTagGroup>> tagGroups) {
Set<EmotivaSubscriptionTagGroup> unsubscribeSet = new HashSet<>();
Set<EmotivaSubscriptionTagGroup> subscribeSet = new HashSet<>();
switch (source) {
case analog1, analog2, analog3, analog4, analog5, analog71, ARC, coax1, coax2, coax3, coax4, front_in,
optical1, optical2, optical3, optical4, usb_stream -> {
subscribeSet.add(EmotivaSubscriptionTagGroup.AUDIO_INFO);
unsubscribeSet.add(EmotivaSubscriptionTagGroup.TUNER);
unsubscribeSet.add(EmotivaSubscriptionTagGroup.VIDEO_INFO);
}
case hdmi1, hdmi2, hdmi3, hdmi4, hdmi5, hdmi6, hdmi7, hdmi8, source_1, source_2, source_3, source_4,
source_5, source_6, source_7, source_8 -> {
subscribeSet.add(EmotivaSubscriptionTagGroup.AUDIO_INFO);
subscribeSet.add(EmotivaSubscriptionTagGroup.VIDEO_INFO);
unsubscribeSet.add(EmotivaSubscriptionTagGroup.TUNER);
}
case tuner -> {
subscribeSet.add(EmotivaSubscriptionTagGroup.TUNER);
subscribeSet.add(EmotivaSubscriptionTagGroup.AUDIO_INFO);
unsubscribeSet.add(EmotivaSubscriptionTagGroup.VIDEO_INFO);
}
default -> subscribeSet.add(EmotivaSubscriptionTagGroup.VIDEO_INFO);
}
tagGroups.accept(subscribeSet, unsubscribeSet);
}
}
@@ -33,6 +33,7 @@ import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.STRI
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.UNKNOWN;
import java.util.EnumMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -202,18 +203,62 @@ public enum EmotivaControlCommands {
this.dataType = dataType;
}
public static EmotivaControlCommands matchToInput(String inputName) {
for (EmotivaControlCommands value : values()) {
if (inputName.toLowerCase().equals(value.name())) {
return value;
/**
* Matches given tag to Emotiva command using the following rules:
*
* 1. Check if tag name matches any known {@link EmotivaControlCommands} command.
* 2. Check if tag name has a corresponding {@link EmotivaControlCommands} source command (input_ -> source_).
*
* @param tag Input tag
* @return Command matched from tag or else {@link EmotivaControlCommands#none}
*/
public static EmotivaControlCommands matchFromSourceInput(EmotivaSubscriptionTags tag) {
for (EmotivaControlCommands command : values()) {
if (tag.name().equalsIgnoreCase(command.name())) {
return command;
}
}
if (inputName.startsWith("input_")) {
return valueOf(inputName.replace("input_", "source_"));
if (tag.name().toLowerCase().startsWith("input_")) {
return valueOf(tag.name().toLowerCase().replace("input_", "source_"));
}
return none;
}
/**
* Matches a string input to a {@link EmotivaControlCommands} source command, uses the rules listed below for
* matching. User customised labels might have equal content for multiple sources, so it is up to the user to keep
* each unique, if not the first in the set is returned.
*
* 1. First match between of input label and label from map.
* 2. Check if input matches name of any command in map.
* 3. Check if input matches name of any known {@link EmotivaControlCommands}.
*
* @param input Label or command input
* @param sourceCommandToLabelMap Source to label map
* @return Command matched from input or else {@link EmotivaControlCommands#none}
*/
public static EmotivaControlCommands matchFromSourceInput(String input,
Map<EmotivaControlCommands, String> sourceCommandToLabelMap) {
// Cleanup input, Emotiva labels might have spaces and REST API might add line breaks
String inputTrimmed = input.trim().replaceAll("(\r\n|\n)", "");
for (EmotivaControlCommands command : sourceCommandToLabelMap.keySet()) {
String label = sourceCommandToLabelMap.get(command);
if (label != null && label.equals(inputTrimmed)) {
return command;
} else if (command.name().equalsIgnoreCase(inputTrimmed)) {
return command;
}
}
for (EmotivaControlCommands command : values()) {
// Check if input label matches label of the current command list
if (inputTrimmed.equalsIgnoreCase(command.name())) {
return command;
}
}
return EmotivaControlCommands.none;
}
public String getLabel() {
return label;
}
@@ -226,13 +271,17 @@ public enum EmotivaControlCommands {
return dataType;
}
public static EnumMap<EmotivaControlCommands, String> getCommandsFromType(EmotivaCommandType filter) {
/**
* Provides map of Emotiva command to label for given {@link EmotivaCommandType}.
*
* @param filter Filter provided for reducing the map to a given type
* @return Map
*/
public static EnumMap<EmotivaControlCommands, String> getByCommandType(EmotivaCommandType filter) {
EnumMap<EmotivaControlCommands, String> commands = new EnumMap<>(EmotivaControlCommands.class);
for (EmotivaControlCommands value : values()) {
if (value.getCommandType().equals(filter)) {
var sb = new StringBuilder(value.name());
sb.setCharAt(0, Character.toUpperCase(value.name().charAt(0)));
commands.put(value, sb.toString());
for (EmotivaControlCommands command : values()) {
if (command.getCommandType().equals(filter)) {
commands.put(command, command.label);
}
}
return commands;
@@ -16,10 +16,9 @@ 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.EmotivaControlCommands.none;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.FREQUENCY_HERTZ;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.emotiva.internal.EmotivaProcessorState;
import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
@@ -168,16 +167,16 @@ public class EmotivaControlRequest {
case NONE -> {
switch (channel) {
case CHANNEL_TUNER_BAND -> {
return matchToCommandMap(ohCommand, MAP_TUNER_BANDS);
return getDTOFromCommandInMap(ohCommand, MAP_TUNER_BANDS);
}
case CHANNEL_TUNER_CHANNEL_SELECT -> {
return matchToCommandMap(ohCommand, MAP_TUNER_CHANNELS);
return getDTOFromCommandInMap(ohCommand, MAP_TUNER_CHANNELS);
}
case CHANNEL_SOURCE -> {
return matchToCommandMap(ohCommand, MAP_SOURCES_MAIN_ZONE);
return getDTOFromCommandInMap(ohCommand, MAP_SOURCES_MAIN_ZONE);
}
case CHANNEL_ZONE2_SOURCE -> {
return matchToCommandMap(ohCommand, MAP_SOURCES_ZONE_2);
return getDTOFromCommandInMap(ohCommand, MAP_SOURCES_ZONE_2);
}
default -> {
logger.debug(
@@ -325,21 +324,19 @@ public class EmotivaControlRequest {
}
}
private EmotivaControlDTO matchToCommandMap(Command ohCommand, String mapName) {
if (ohCommand instanceof StringType value) {
Map<EmotivaControlCommands, String> commandMap = state.getCommandMap(mapName);
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);
}
public EmotivaControlDTO getDTOFromCommandInMap(Command ohCommand, String mapName) {
if (ohCommand instanceof StringType ohCommandValue) {
EmotivaControlCommands command = EmotivaControlCommands.matchFromSourceInput(ohCommandValue.toString(),
state.getCommandMap(mapName));
if (command.equals(none)) {
logger.debug("Could not match OH command {}:{} with value '{}' to '{}' map, no matching command",
channel, ohCommand.getClass().getSimpleName(), ohCommand.toFullString(), mapName);
}
return EmotivaControlDTO.create(command);
} else {
logger.debug("Could not match OH command {}:{} with value '{}' to '{}' map , not a OH StringType", channel,
ohCommand.getClass().getSimpleName(), ohCommand.toFullString(), mapName);
}
logger.debug(
"OH command {}:{} mapped to Emotiva Command type {} could not be matched to a Emotiva control command, not a OH String type",
channel, ohCommand.getClass().getSimpleName(), NONE);
return EmotivaControlDTO.create(EmotivaControlCommands.none);
}
@@ -514,7 +511,6 @@ public class EmotivaControlRequest {
return "EmotivaControlRequest{" + "name='" + name + '\'' + ", dataType=" + dataType + ", channel='" + channel
+ '\'' + ", defaultCommand=" + defaultCommand + ", setCommand=" + setCommand + ", onCommand="
+ onCommand + ", offCommand=" + offCommand + ", upCommand=" + upCommand + ", downCommand=" + downCommand
+ ", maxValue=" + maxValue + ", minValue=" + minValue + ", state=" + state + ", protocolVersion="
+ protocolVersion + '}';
+ ", maxValue=" + maxValue + ", minValue=" + minValue + ", protocolVersion=" + protocolVersion + '}';
}
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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;
/**
* Emotiva subscription tag group. Each Emotiva subscription tag is assigned to group in
* {@link EmotivaSubscriptionTags} for the possibility of handling via
* {@link org.openhab.binding.emotiva.internal.EmotivaSubscriptionTagGroupHandler}.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public enum EmotivaSubscriptionTagGroup {
AUDIO_ADJUSTMENT,
AUDIO_INFO,
GENERAL,
SOURCES,
TUNER,
UI_DEVICE,
UI_MENU,
VIDEO_INFO,
NONE,
ZONE2_GENERAL
}
@@ -14,9 +14,11 @@ 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 static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTagGroup.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
@@ -31,70 +33,70 @@ import org.slf4j.LoggerFactory;
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),
power("power", ON_OFF, CHANNEL_MAIN_ZONE_POWER, GENERAL),
source("source", STRING, CHANNEL_SOURCE, GENERAL),
dim("dim", DIMENSIONLESS_PERCENT, CHANNEL_DIM, UI_DEVICE),
mode("mode", STRING, CHANNEL_MODE, GENERAL),
speaker_preset("speaker-preset", STRING, CHANNEL_SPEAKER_PRESET, AUDIO_ADJUSTMENT),
center("center", DIMENSIONLESS_DECIBEL, CHANNEL_CENTER, AUDIO_ADJUSTMENT),
subwoofer("subwoofer", DIMENSIONLESS_DECIBEL, CHANNEL_SUBWOOFER, AUDIO_ADJUSTMENT),
surround("surround", DIMENSIONLESS_DECIBEL, CHANNEL_SURROUND, AUDIO_ADJUSTMENT),
back("back", DIMENSIONLESS_DECIBEL, CHANNEL_BACK, AUDIO_ADJUSTMENT),
volume("volume", DIMENSIONLESS_DECIBEL, CHANNEL_MAIN_VOLUME, GENERAL),
loudness("loudness", ON_OFF, CHANNEL_LOUDNESS, AUDIO_ADJUSTMENT),
treble("treble", DIMENSIONLESS_DECIBEL, CHANNEL_TREBLE, AUDIO_ADJUSTMENT),
bass("bass", DIMENSIONLESS_DECIBEL, CHANNEL_BASS, AUDIO_ADJUSTMENT),
zone2_power("zone2-power", ON_OFF, CHANNEL_ZONE2_POWER, ZONE2_GENERAL),
zone2_volume("zone2-volume", DIMENSIONLESS_DECIBEL, CHANNEL_ZONE2_VOLUME, ZONE2_GENERAL),
zone2_input("zone2-input", STRING, CHANNEL_ZONE2_SOURCE, ZONE2_GENERAL),
tuner_band("tuner-band", STRING, CHANNEL_TUNER_BAND, TUNER),
tuner_channel("tuner-channel", FREQUENCY_HERTZ, CHANNEL_TUNER_CHANNEL, TUNER),
tuner_signal("tuner-signal", STRING, CHANNEL_TUNER_SIGNAL, TUNER),
tuner_program("tuner-program", STRING, CHANNEL_TUNER_PROGRAM, TUNER),
tuner_RDS("tuner-RDS", STRING, CHANNEL_TUNER_RDS, TUNER),
audio_input("audio-input", STRING, CHANNEL_AUDIO_INPUT, AUDIO_INFO),
audio_bitstream("audio-bitstream", STRING, CHANNEL_AUDIO_BITSTREAM, AUDIO_INFO),
audio_bits("audio-bits", STRING, CHANNEL_AUDIO_BITS, AUDIO_INFO),
video_input("video-input", STRING, CHANNEL_VIDEO_INPUT, VIDEO_INFO),
video_format("video-format", STRING, CHANNEL_VIDEO_FORMAT, VIDEO_INFO),
video_space("video-space", STRING, CHANNEL_VIDEO_SPACE, VIDEO_INFO),
input_1("input-1", STRING, CHANNEL_INPUT1, SOURCES),
input_2("input-2", STRING, CHANNEL_INPUT2, SOURCES),
input_3("input-3", STRING, CHANNEL_INPUT3, SOURCES),
input_4("input-4", STRING, CHANNEL_INPUT4, SOURCES),
input_5("input-5", STRING, CHANNEL_INPUT5, SOURCES),
input_6("input-6", STRING, CHANNEL_INPUT6, SOURCES),
input_7("input-7", STRING, CHANNEL_INPUT7, SOURCES),
input_8("input-8", STRING, CHANNEL_INPUT8, SOURCES),
/* 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),
selected_mode("selected-mode", STRING, CHANNEL_SELECTED_MODE, GENERAL),
selected_movie_music("selected-movie-music", STRING, CHANNEL_SELECTED_MOVIE_MUSIC, GENERAL),
mode_ref_stereo("mode-ref-stereo", STRING, CHANNEL_MODE_REF_STEREO, SOURCES),
mode_stereo("mode-stereo", STRING, CHANNEL_MODE_STEREO, SOURCES),
mode_music("mode-music", STRING, CHANNEL_MODE_MUSIC, SOURCES),
mode_movie("mode-movie", STRING, CHANNEL_MODE_MOVIE, SOURCES),
mode_direct("mode-direct", STRING, CHANNEL_MODE_DIRECT, SOURCES),
mode_dolby("mode-dolby", STRING, CHANNEL_MODE_DOLBY, SOURCES),
mode_dts("mode-dts", STRING, CHANNEL_MODE_DTS, SOURCES),
mode_all_stereo("mode-all-stereo", STRING, CHANNEL_MODE_ALL_STEREO, SOURCES),
mode_auto("mode-auto", STRING, CHANNEL_MODE_AUTO, SOURCES),
mode_surround("mode-surround", STRING, CHANNEL_MODE_SURROUND, SOURCES),
menu("menu", ON_OFF, CHANNEL_MENU, UI_MENU),
menu_update("menu-update", STRING, CHANNEL_MENU_DISPLAY_PREFIX, UI_MENU),
/* Protocol V3 notify tags */
keepAlive("keepAlive", NUMBER_TIME, ""),
goodBye("goodBye", GOODBYE, ""),
bar_update("bar-update", STRING, CHANNEL_BAR),
width("width", DIMENSIONLESS_DECIBEL, CHANNEL_WIDTH),
height("height", DIMENSIONLESS_DECIBEL, CHANNEL_HEIGHT),
keepAlive("keepAlive", NUMBER_TIME, "", GENERAL),
goodBye("goodBye", GOODBYE, "", GENERAL),
bar_update("bar-update", STRING, CHANNEL_BAR, UI_DEVICE),
width("width", DIMENSIONLESS_DECIBEL, CHANNEL_WIDTH, AUDIO_ADJUSTMENT),
height("height", DIMENSIONLESS_DECIBEL, CHANNEL_HEIGHT, AUDIO_ADJUSTMENT),
/* Notify tag not in the documentation */
source_tuner("source-tuner", ON_OFF, ""),
source_tuner("source-tuner", ON_OFF, "", SOURCES),
/* No match tag */
unknown("unknown", UNKNOWN, "");
unknown("unknown", UNKNOWN, "", EmotivaSubscriptionTagGroup.NONE);
private final Logger logger = LoggerFactory.getLogger(EmotivaSubscriptionTags.class);
@@ -104,11 +106,14 @@ public enum EmotivaSubscriptionTags {
private final String name;
private final EmotivaDataType dataType;
private final String channel;
private final EmotivaSubscriptionTagGroup subscriptionTagGroup;
EmotivaSubscriptionTags(String name, EmotivaDataType dataType, String channel) {
EmotivaSubscriptionTags(String name, EmotivaDataType dataType, String channel,
EmotivaSubscriptionTagGroup subscriptionTagGroup) {
this.name = name;
this.dataType = dataType;
this.channel = channel;
this.subscriptionTagGroup = subscriptionTagGroup;
}
public static boolean hasChannel(String name) {
@@ -157,8 +162,16 @@ public enum EmotivaSubscriptionTags {
return tags.toArray(new EmotivaSubscriptionTags[0]);
}
public static List<EmotivaSubscriptionTags> noSubscriptionToChannel() {
return List.of(goodBye);
public static List<EmotivaSubscriptionTags> getBySubscriptionTagGroups(Set<EmotivaSubscriptionTagGroup> groups) {
List<EmotivaSubscriptionTags> tags = new ArrayList<>();
for (EmotivaSubscriptionTagGroup group : groups) {
for (EmotivaSubscriptionTags value : values()) {
if (value.subscriptionTagGroup.equals(group)) {
tags.add(value);
}
}
}
return tags;
}
public String getName() {
@@ -167,7 +180,7 @@ public enum EmotivaSubscriptionTags {
public String getEmotivaName() {
String retVal = name.replaceAll("-", "_");
logger.debug("Converting OH channel '{}' to Emotiva command '{}'", name, retVal);
logger.trace("Converting OH channel '{}' to Emotiva command '{}'", name, retVal);
return retVal;
}
@@ -45,6 +45,27 @@
<default>7100</default>
<advanced>true</advanced>
</parameter>
<parameter name="activateFrontBar" type="boolean" required="false">
<context>feature</context>
<label>Enable Front Bar Channels</label>
<description>Subscribe to notification related to front bar</description>
<default>false</default>
<advanced>false</advanced>
</parameter>
<parameter name="activateOSDMenu" type="boolean" required="false">
<context>feature</context>
<label>Enable Menu Channels</label>
<description>Subscribe to notification related to OSD menu</description>
<default>false</default>
<advanced>false</advanced>
</parameter>
<parameter name="activateZone2" type="boolean" required="false">
<context>feature</context>
<label>Enable Zone 2 Channels</label>
<description>Subscribe to notification related to zone 2</description>
<default>false</default>
<advanced>false</advanced>
</parameter>
<parameter name="protocolVersion" type="text" required="false">
<context>protocol-revision</context>
<label>Protocol Version</label>
@@ -22,6 +22,12 @@ 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.activateFrontBar.label = Front Bar Notifications
thing-type.config.emotiva.config.activateFrontBar.description = Activate notification for front bar channel
thing-type.config.emotiva.config.activateOSDMenu.label = OSD Menu Notifications
thing-type.config.emotiva.config.activateOSDMenu.description = Activate notification for menu channel
thing-type.config.emotiva.config.activateZone2.label = Zone 2 Notifications
thing-type.config.emotiva.config.activateZone2.description = Activate notification for Zone 2 channels
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
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.BINDING_ID;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.THING_PROCESSOR;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.coax1;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.hdmi1;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.source_1;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.source_2;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags.source;
import java.util.EnumMap;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.StateOption;
/**
* Unit tests for {@link EmotivaInputStateOptionProvider}.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaInputStateOptionProviderTest {
@Test
void initialMainZoneSourcesToStateOptionsWithCustomLabels() {
EnumMap<EmotivaControlCommands, String> mainZoneSourcesMap = new EnumMap<>(EmotivaControlCommands.class);
mainZoneSourcesMap.put(source_1, "Input 1");
mainZoneSourcesMap.put(source_2, "SHIELD");
mainZoneSourcesMap.put(hdmi1, "HDMI 1");
mainZoneSourcesMap.put(coax1, "Coax 1");
EmotivaProcessorState state = new EmotivaProcessorState();
state.setSourcesMainZone(mainZoneSourcesMap);
EmotivaInputStateOptionProvider provider = new EmotivaInputStateOptionProvider(mock(EventPublisher.class),
mock(ItemChannelLinkRegistry.class), mock(ChannelTypeI18nLocalizationService.class));
ThingUID thingUID = new ThingUID(THING_PROCESSOR, "XMC-2");
ChannelUID channelUID = new ChannelUID(new ChannelGroupUID(thingUID, "main-zone"), source.name());
Channel channel = ChannelBuilder.create(channelUID).withType(new ChannelTypeUID(BINDING_ID, source.name()))
.build();
List<StateOption> stateOptions = provider.gatherStateOptionsForSource(channel, mainZoneSourcesMap);
assertThat(stateOptions, not(nullValue()));
assertThat(stateOptions.size(), is(4));
assertThat(stateOptions.get(0).getValue(), is(source_1.name()));
assertThat(stateOptions.get(0).getLabel(), is("Input 1"));
assertThat(stateOptions.get(1).getValue(), is(source_2.name()));
assertThat(stateOptions.get(1).getLabel(), is("SHIELD")); // Overridden by user
assertThat(stateOptions.get(2).getValue(), is(hdmi1.name()));
assertThat(stateOptions.get(2).getLabel(), is("HDMI 1"));
assertThat(stateOptions.get(3).getValue(), is(coax1.name()));
assertThat(stateOptions.get(3).getLabel(), is("Coax 1"));
}
@Test
void updatedMaineZoneSourcesToStateOptionsWithCustomLabels() {
EnumMap<EmotivaControlCommands, String> mainZoneSourcesMap = new EnumMap<>(EmotivaControlCommands.class);
mainZoneSourcesMap.put(source_1, "Input 1");
mainZoneSourcesMap.put(source_2, "SHIELD");
mainZoneSourcesMap.put(hdmi1, "HDMI 1");
mainZoneSourcesMap.put(coax1, "Coax 1");
EmotivaProcessorState state = new EmotivaProcessorState();
state.setSourcesMainZone(mainZoneSourcesMap);
EmotivaInputStateOptionProvider provider = new EmotivaInputStateOptionProvider(mock(EventPublisher.class),
mock(ItemChannelLinkRegistry.class), mock(ChannelTypeI18nLocalizationService.class));
ThingUID thingUID = new ThingUID(THING_PROCESSOR, "XMC-2");
ChannelUID channelUID = new ChannelUID(new ChannelGroupUID(thingUID, "main-zone"), source.name());
Channel channel = ChannelBuilder.create(channelUID).withType(new ChannelTypeUID(BINDING_ID, source.name()))
.build();
String sourceOneLabel = "Should be added to options";
state.updateSourcesMainZone(EmotivaControlCommands.source_1, sourceOneLabel);
state.updateSourcesMainZone(EmotivaControlCommands.hdmi1, "Should not be added to options");
List<StateOption> stateOptions = provider.gatherStateOptionsForSource(channel, mainZoneSourcesMap);
assertThat(stateOptions, not(nullValue()));
assertThat(stateOptions.size(), is(4));
assertThat(stateOptions.get(0), not(nullValue()));
assertThat(stateOptions.get(0).getValue(), is(source_1.name()));
assertThat(stateOptions.get(0).getLabel(), is(sourceOneLabel));
assertThat(stateOptions.get(1).getValue(), is(source_2.name()));
assertThat(stateOptions.get(1).getLabel(), is("SHIELD")); // Overridden by user
assertThat(stateOptions.get(2).getValue(), is(hdmi1.name()));
assertThat(stateOptions.get(2).getLabel(), is("HDMI 1"));
assertThat(stateOptions.get(3).getValue(), is(coax1.name()));
assertThat(stateOptions.get(3).getLabel(), is("Coax 1"));
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.not;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTagGroup.AUDIO_INFO;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTagGroup.TUNER;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTagGroup.VIDEO_INFO;
import java.util.Set;
import java.util.function.BiConsumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTagGroup;
/**
* Unit tests for {@link EmotivaSubscriptionTagGroupHandler}.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaSubscriptionTagGroupHandlerTest {
@Test
void sourceTuner() {
EmotivaConfiguration config = new EmotivaConfiguration();
EmotivaProcessorState state = new EmotivaProcessorState();
var subscriptionHandler = new EmotivaSubscriptionTagGroupHandler(config, state);
BiConsumer<Set<EmotivaSubscriptionTagGroup>, Set<EmotivaSubscriptionTagGroup>> tagGroups = (subscribeSet,
unsubscribeSet) -> {
assertThat(subscribeSet, hasItems(TUNER));
assertThat(subscribeSet, not(hasItems(VIDEO_INFO)));
assertThat(unsubscribeSet, hasItems(VIDEO_INFO));
assertThat(unsubscribeSet, not(hasItems(TUNER)));
};
subscriptionHandler.tagGroupsFromSource(EmotivaControlCommands.tuner, tagGroups);
}
@Test
void sourceAudioOnly() {
EmotivaConfiguration config = new EmotivaConfiguration();
EmotivaProcessorState state = new EmotivaProcessorState();
var subscriptionHandler = new EmotivaSubscriptionTagGroupHandler(config, state);
BiConsumer<Set<EmotivaSubscriptionTagGroup>, Set<EmotivaSubscriptionTagGroup>> tagGroups = (subscribeSet,
unsubscribeSet) -> {
assertThat(subscribeSet, hasItems(AUDIO_INFO));
assertThat(subscribeSet, not(hasItems(TUNER)));
assertThat(unsubscribeSet, hasItems(TUNER));
assertThat(unsubscribeSet, not(hasItems(AUDIO_INFO)));
};
subscriptionHandler.tagGroupsFromSource(EmotivaControlCommands.analog1, tagGroups);
}
@Test
void sourceAudioAndVideo() {
EmotivaConfiguration config = new EmotivaConfiguration();
EmotivaProcessorState state = new EmotivaProcessorState();
var subscriptionHandler = new EmotivaSubscriptionTagGroupHandler(config, state);
BiConsumer<Set<EmotivaSubscriptionTagGroup>, Set<EmotivaSubscriptionTagGroup>> tagGroups = (subscribeSet,
unsubscribeSet) -> {
assertThat(subscribeSet, hasItems(AUDIO_INFO));
assertThat(subscribeSet, hasItems(VIDEO_INFO));
assertThat(subscribeSet, not(hasItems(TUNER)));
assertThat(unsubscribeSet, hasItems(TUNER));
assertThat(unsubscribeSet, not(hasItems(AUDIO_INFO)));
assertThat(unsubscribeSet, not(hasItems(VIDEO_INFO)));
};
subscriptionHandler.tagGroupsFromSource(EmotivaControlCommands.hdmi1, tagGroups);
}
}
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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_FREQUENCY;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TREBLE;
import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.CHANNEL_TUNER_CHANNEL;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.*;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.band_am;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.band_fm;
import java.util.EnumMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeAll;
import org.openhab.binding.emotiva.internal.EmotivaProcessorState;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
/**
* Common setup for tests related to the {@link EmotivaControlCommands}.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
public class AbstractEmotivaControlTest {
protected static final EnumMap<EmotivaControlCommands, String> MAP_SOURCES_MAIN_ZONE = new EnumMap<>(
EmotivaControlCommands.class);
protected static final EnumMap<EmotivaControlCommands, String> MAP_SOURCES_ZONE_2 = new EnumMap<>(
EmotivaControlCommands.class);
protected static final EnumMap<EmotivaControlCommands, String> CHANNEL_MAP = new EnumMap<>(
EmotivaControlCommands.class);
protected static final EnumMap<EmotivaControlCommands, String> RADIO_BAND_MAP = new EnumMap<>(
EmotivaControlCommands.class);
protected static final EmotivaProcessorState state = new EmotivaProcessorState();
@BeforeAll
static void beforeAll() {
MAP_SOURCES_MAIN_ZONE.put(source_1, "Input 1");
MAP_SOURCES_MAIN_ZONE.put(source_2, "SHIELD");
MAP_SOURCES_MAIN_ZONE.put(hdmi1, "HDMI 1");
MAP_SOURCES_MAIN_ZONE.put(coax1, "Coax 1");
state.setSourcesMainZone(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, "HDMI 1");
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");
state.setSourcesZone2(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");
state.setChannels(CHANNEL_MAP);
RADIO_BAND_MAP.put(band_am, "AM");
RADIO_BAND_MAP.put(band_fm, "FM");
state.setTunerBands(RADIO_BAND_MAP);
state.updateChannel(CHANNEL_TREBLE, new DecimalType(-3));
state.updateChannel(CHANNEL_TUNER_CHANNEL, new StringType("FM 87.50MHz"));
state.updateChannel(CHANNEL_FREQUENCY, QuantityType.valueOf(107.90, Units.HERTZ));
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 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.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.analog1;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.hdmi1;
import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands.source_2;
import java.util.Map;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Unit tests for {@link EmotivaControlCommands}.
*
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaControlCommandsTest extends AbstractEmotivaControlTest {
private static Stream<Arguments> mapInputToSourceCommand() {
return Stream.of(Arguments.of("", "none", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of(" ", "none", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of(" ", "none", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of("HDMI1", "hdmi1", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of("HDMI1\n", "hdmi1", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of("HDMI1\r", "hdmi1", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of("\n\rHDMI1\r\n", "hdmi1", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of("Hdmi1", "hdmi1", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of(" Hdmi1 ", "hdmi1", Map.ofEntries(Map.entry(hdmi1, hdmi1.getLabel()))),
Arguments.of("source_2", "source_2", Map.ofEntries(Map.entry(source_2, "SHIELD"))),
Arguments.of("SHIELD", "source_2", Map.ofEntries(Map.entry(source_2, "SHIELD"))),
Arguments.of("Analog 1", "analog1", Map.ofEntries(Map.entry(analog1, "Analog 1"))),
Arguments.of("Analog1", "analog1", Map.ofEntries(Map.entry(analog1, "Analog1"))));
}
@ParameterizedTest
@MethodSource("mapInputToSourceCommand")
void mapInputToSourceCommand2(String input, String expected,
Map<EmotivaControlCommands, String> mainZoneSourceOverrides) {
mainZoneSourceOverrides.forEach(state::updateSourcesMainZone);
EmotivaControlCommands sourceCommand = EmotivaControlCommands.matchFromSourceInput(input,
mainZoneSourceOverrides);
assertThat(sourceCommand.name(), is(expected));
}
}
@@ -19,17 +19,14 @@ import static org.openhab.binding.emotiva.internal.protocol.EmotivaControlComman
import static org.openhab.binding.emotiva.internal.protocol.EmotivaProtocolVersion.*;
import static org.openhab.core.types.RefreshType.REFRESH;
import java.util.EnumMap;
import java.util.Optional;
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.EmotivaCommandHelper;
import org.openhab.binding.emotiva.internal.EmotivaProcessorState;
import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@@ -48,7 +45,7 @@ import org.openhab.core.types.UnDefType;
* @author Espen Fossen - Initial contribution
*/
@NonNullByDefault
class EmotivaControlRequestTest {
class EmotivaControlRequestTest extends AbstractEmotivaControlTest {
private static Stream<Arguments> channelToDTOs() {
return Stream.of(Arguments.of(CHANNEL_STANDBY, OnOffType.ON, standby, PROTOCOL_V2, "0"),
@@ -132,7 +129,7 @@ class EmotivaControlRequestTest {
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("coax1"), coax1, 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"),
@@ -207,46 +204,6 @@ class EmotivaControlRequestTest {
"-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 EmotivaProcessorState state = new EmotivaProcessorState();
@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");
state.setSourcesMainZone(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");
state.setSourcesZone2(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");
state.setChannels(CHANNEL_MAP);
RADIO_BAND_MAP.put(band_am, "AM");
RADIO_BAND_MAP.put(band_fm, "FM");
state.setTunerBands(RADIO_BAND_MAP);
state.updateChannel(CHANNEL_TREBLE, new DecimalType(-3));
state.updateChannel(CHANNEL_TUNER_CHANNEL, new StringType("FM 87.50MHz"));
state.updateChannel(CHANNEL_FREQUENCY, QuantityType.valueOf(107.90, Units.HERTZ));
}
@ParameterizedTest
@MethodSource("channelToDTOs")
void createDTO(String channel, Command ohValue, EmotivaControlCommands controlCommand,