]> git.basschouten.com Git - openhab-addons.git/commitdiff
[emotiva] Fix missing data in source channels (#17365)
authorEspen Fossen <espenaf@junta.no>
Sat, 28 Sep 2024 13:28:35 +0000 (15:28 +0200)
committerGitHub <noreply@github.com>
Sat, 28 Sep 2024 13:28:35 +0000 (15:28 +0200)
* [emotiva] Fixes issue with missing data in source channels.

Signed-off-by: Espen Fossen <espenaf@junta.no>
16 files changed:
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaBindingConstants.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelper.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorHandler.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorState.java [new file with mode: 0644]
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpReceivingService.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaUdpSendingService.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/InputStateOptionProvider.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequest.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscribeDTO.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequest.java
bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/protocol/EmotivaSubscriptionTags.java
bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaCommandHelperTest.java
bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaProcessorStateTest.java [new file with mode: 0644]
bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaSubscriptionRequestTest.java
bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/dto/EmotivaUnsubscriptionTest.java
bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/protocol/EmotivaControlRequestTest.java

index 33d9fd0596eb390d223aa2cd8ff1ebb762739041..316e941606aa8108bc6204b0ae35a91729a6f09a 100644 (file)
@@ -86,6 +86,9 @@ public class EmotivaBindingConstants {
     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";
+    public static final String MAP_TUNER_CHANNELS = "tuner-channel";
+    public static final String MAP_TUNER_BANDS = "tuner-bands";
+    public static final String MAP_MODES = "modes";
 
     /** Miscellaneous Constants **/
     public static final int PROTOCOL_V3_LEVEL_MULTIPLIER = 2;
index ec79969e8531021a7ff0d0e37b57dd2dacda1453..ab3e870c1144413f8d9d23c8692fc9f82de95b41 100644 (file)
@@ -14,8 +14,6 @@ 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;
@@ -69,11 +67,11 @@ public class EmotivaCommandHelper {
         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) {
+    public static EmotivaControlRequest channelToControlRequest(String id, EmotivaProcessorState state,
+            EmotivaProtocolVersion protocolVersion) {
         EmotivaSubscriptionTags channelSubscription = EmotivaSubscriptionTags.fromChannelUID(id);
         EmotivaControlCommands channelFromCommand = OHChannelToEmotivaCommand.fromChannelUID(id);
-        return new EmotivaControlRequest(id, channelSubscription, channelFromCommand, commandMaps, protocolVersion);
+        return new EmotivaControlRequest(id, channelSubscription, channelFromCommand, state, protocolVersion);
     }
 
     public static String getMenuPanelRowLabel(int row) {
index 9b1d07becf04db537d812b348beb6aef761ba7ee..191a96f1350002875a6bd68190b1e87ecc7951cf 100644 (file)
@@ -20,17 +20,12 @@ import static org.openhab.binding.emotiva.internal.EmotivaCommandHelper.getMenuP
 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;
@@ -40,13 +35,10 @@ 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.Optional;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
@@ -74,6 +66,7 @@ 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.DecimalType;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.PercentType;
 import org.openhab.core.library.types.QuantityType;
@@ -102,21 +95,17 @@ 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.
+     * groups.
      */
-    private final EmotivaSubscriptionTags[] generalSubscription = EmotivaSubscriptionTags.generalChannels();
-    private final EmotivaSubscriptionTags[] nonGeneralSubscriptions = EmotivaSubscriptionTags.nonGeneralChannels();
+    private final List<EmotivaSubscriptionTags> generalSubscription = EmotivaSubscriptionTags.channels("general");
+    private final List<EmotivaSubscriptionTags> mainZoneSubscriptions = EmotivaSubscriptionTags.channels("main-zone");
+    private final List<EmotivaSubscriptionTags> zone2Subscriptions = EmotivaSubscriptionTags.channels("zone2");
 
-    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 EmotivaProcessorState state = new EmotivaProcessorState();
     private final EmotivaTranslationProvider i18nProvider;
 
     private @Nullable ScheduledFuture<?> pollingJob;
@@ -141,41 +130,6 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
         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
@@ -222,7 +176,8 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
                 try {
                     logger.debug("Connection attempt '{}'", attempt);
                     sendConnector.sendSubscription(generalSubscription, config);
-                    sendConnector.sendSubscription(nonGeneralSubscriptions, config);
+                    sendConnector.sendSubscription(mainZoneSubscriptions, config);
+                    sendConnector.sendSubscription(zone2Subscriptions, config);
                 } catch (IOException e) {
                     // network or socket failure, also wait 2 sec and try again
                 }
@@ -264,44 +219,49 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
     }
 
     /**
-     * Starts a polling job for connection to th device, adds the
+     * Starts a polling job for connection to the 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,
+
+            Number keepAliveConfig = state.getChannel(EmotivaSubscriptionTags.keepAlive)
+                    .filter(channel -> channel instanceof Number).map(keepAlive -> (Number) keepAlive)
+                    .orElse(new DecimalType(config.keepAlive));
+
+            // noinspection ConstantConditions
+            long delay = keepAliveConfig == null
+                    ? DEFAULT_KEEP_ALIVE_IN_MILLISECONDS + DEFAULT_KEEP_ALIVE_IN_MILLISECONDS
+                    : keepAliveConfig.longValue() + DEFAULT_KEEP_ALIVE_IN_MILLISECONDS;
+            pollingJob = scheduler.scheduleWithFixedDelay(this::checkKeepAliveTimestamp, delay, delay,
                     TimeUnit.MILLISECONDS);
+            logger.debug("Started scheduled job to check connection to device, with an {}ms internal", delay);
         }
     }
 
     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);
+            Optional<State> lastSeenState = state.getChannel(LAST_SEEN_STATE_NAME);
+            if (lastSeenState.isPresent()) {
+                if (lastSeenState.get() 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())) {
@@ -325,9 +285,10 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
             return;
         }
 
-        if (object instanceof EmotivaAckDTO answerDto) {
+        if (object instanceof EmotivaAckDTO) {
             // Currently not supported to revert a failed command update, just used for logging for now.
-            logger.trace("Processing received '{}' with '{}'", EmotivaAckDTO.class.getSimpleName(), answerDto);
+            logger.trace("Processing received '{}' with '{}'", EmotivaAckDTO.class.getSimpleName(),
+                    emotivaUdpResponse.answer());
         } else if (object instanceof EmotivaBarNotifyWrapper answerDto) {
             logger.trace("Processing received '{}' with '{}'", EmotivaBarNotifyWrapper.class.getSimpleName(),
                     emotivaUdpResponse.answer());
@@ -364,13 +325,15 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
             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);
+            EnumMap<EmotivaControlCommands, String> sourceMainZone = EmotivaControlCommands
+                    .getCommandsFromType(EmotivaCommandType.SOURCE_MAIN_ZONE);
+            sourceMainZone.remove(EmotivaControlCommands.input);
+            state.setSourcesMainZone(sourceMainZone);
 
-            sourcesZone2.putAll(EmotivaControlCommands.getCommandsFromType(EmotivaCommandType.SOURCE_ZONE2));
-            sourcesZone2.remove(EmotivaControlCommands.zone2_input);
-            commandMaps.put(MAP_SOURCES_ZONE_2, sourcesZone2);
+            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())) {
@@ -528,11 +491,9 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
             // 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());
+                state.updateSourcesMainZone(EmotivaControlCommands.matchToInput(subscriptionTag.name()), trimmedValue);
+                logger.debug("Adding '{}' to dynamic source input list, map is now {}", trimmedValue,
+                        state.getSourcesMainZone());
             }
 
             // Add/Update audio modes
@@ -541,7 +502,7 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
                         + 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);
+                state.updateModes(EmotivaSubscriptionTags.fromChannelUID(subscriptionTag.getChannel()), trimmedValue);
             }
 
             findChannelDatatypeAndUpdateChannel(subscriptionTag.getChannel(), trimmedValue,
@@ -630,10 +591,10 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
         }
     }
 
-    private void updateChannelState(String channelID, State state) {
-        stateMap.put(channelID, state);
-        logger.trace("Updating channel '{}' with '{}'", channelID, state);
-        updateState(channelID, state);
+    private void updateChannelState(String channelID, State channelState) {
+        state.updateChannel(channelID, channelState);
+        logger.trace("Updating channel '{}' with '{}'", channelID, channelState);
+        updateState(channelID, channelState);
     }
 
     private void updateVolumeChannels(String value, String muteChannel, String volumeChannel, String volumeDbChannel) {
@@ -651,10 +612,10 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
         EmotivaUdpSendingService localSendingService = sendingService;
 
         if (localSendingService != null) {
-            EmotivaControlRequest emotivaRequest = channelToControlRequest(channelUID.getId(), commandMaps,
+            EmotivaControlRequest emotivaRequest = channelToControlRequest(channelUID.getId(), state,
                     protocolFromConfig(config.protocolVersion));
             if (ohCommand instanceof RefreshType) {
-                stateMap.remove(channelUID.getId());
+                state.removeChannel(channelUID.getId());
 
                 if (emotivaRequest.getDefaultCommand().equals(none)) {
                     logger.debug("Found controlCommand 'none' for request '{}' from channel '{}' with RefreshType",
@@ -665,26 +626,29 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
                 }
             } 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);
+                    Optional<State> channel = state.getChannel(channelUID.getId());
+                    if (channel.isPresent()) {
+                        EmotivaControlDTO dto = emotivaRequest.createDTO(ohCommand, channel.get());
+                        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) {
@@ -714,7 +678,8 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
                 try {
                     // Unsubscribe before disconnect
                     localSendingService.sendUnsubscribe(generalSubscription);
-                    localSendingService.sendUnsubscribe(nonGeneralSubscriptions);
+                    localSendingService.sendUnsubscribe(mainZoneSubscriptions);
+                    localSendingService.sendUnsubscribe(zone2Subscriptions);
                 } catch (IOException e) {
                     logger.debug("Failed to unsubscribe for '{}'", config.ipAddress, e);
                 }
@@ -772,14 +737,14 @@ public class EmotivaProcessorHandler extends BaseThingHandler {
     }
 
     public EnumMap<EmotivaControlCommands, String> getSourcesMainZone() {
-        return sourcesMainZone;
+        return state.getSourcesMainZone();
     }
 
     public EnumMap<EmotivaControlCommands, String> getSourcesZone2() {
-        return sourcesZone2;
+        return state.getSourcesZone2();
     }
 
     public EnumMap<EmotivaSubscriptionTags, String> getModes() {
-        return modes;
+        return state.getModes();
     }
 }
diff --git a/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorState.java b/bundles/org.openhab.binding.emotiva/src/main/java/org/openhab/binding/emotiva/internal/EmotivaProcessorState.java
new file mode 100644 (file)
index 0000000..6d9f48a
--- /dev/null
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.emotiva.internal;
+
+import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MAP_SOURCES_MAIN_ZONE;
+import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MAP_SOURCES_ZONE_2;
+import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MAP_TUNER_BANDS;
+import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MAP_TUNER_CHANNELS;
+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 java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
+import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
+import org.openhab.core.types.State;
+
+/**
+ * Holds state for Emotiva Processor.
+ *
+ * @author Espen Fossen - Initial contribution
+ */
+@NonNullByDefault
+public class EmotivaProcessorState {
+
+    private final Map<String, State> channelStateMap;
+
+    private EnumMap<EmotivaControlCommands, String> sourcesMainZone;
+    private EnumMap<EmotivaControlCommands, String> sourcesZone2;
+    private final EnumMap<EmotivaSubscriptionTags, String> modes;
+
+    private EnumMap<EmotivaControlCommands, String> tunerChannels = 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())));
+
+    private EnumMap<EmotivaControlCommands, String> tunerBands = new EnumMap<>(
+            Map.of(band_am, band_am.getLabel(), band_fm, band_fm.getLabel()));
+
+    public EmotivaProcessorState() {
+        channelStateMap = Collections.synchronizedMap(new HashMap<>());
+        sourcesMainZone = new EnumMap<>(EmotivaControlCommands.class);
+        sourcesZone2 = new EnumMap<>(EmotivaControlCommands.class);
+        modes = new EnumMap<>(EmotivaSubscriptionTags.class);
+    }
+
+    public Optional<State> getChannel(String channelName) {
+        if (channelStateMap.containsKey(channelName)) {
+            return Optional.ofNullable(channelStateMap.get(channelName));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    public Optional<State> getChannel(EmotivaSubscriptionTags channelTagName) {
+        if (channelStateMap.containsKey(channelTagName.name())) {
+            return Optional.ofNullable(channelStateMap.get(channelTagName.name()));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    public Map<EmotivaControlCommands, String> getCommandMap(String mapName) {
+        return switch (mapName) {
+            case MAP_SOURCES_MAIN_ZONE -> sourcesMainZone;
+            case MAP_SOURCES_ZONE_2 -> sourcesZone2;
+            case MAP_TUNER_CHANNELS -> tunerChannels;
+            case MAP_TUNER_BANDS -> tunerBands;
+            default -> new EnumMap<>(EmotivaControlCommands.class);
+        };
+    }
+
+    public EnumMap<EmotivaControlCommands, String> getSourcesMainZone() {
+        return sourcesMainZone;
+    }
+
+    public EnumMap<EmotivaControlCommands, String> getSourcesZone2() {
+        return sourcesZone2;
+    }
+
+    public EnumMap<EmotivaSubscriptionTags, String> getModes() {
+        return modes;
+    }
+
+    public void setChannels(EnumMap<EmotivaControlCommands, String> map) {
+        tunerChannels = map;
+    }
+
+    public void setSourcesMainZone(EnumMap<EmotivaControlCommands, String> map) {
+        sourcesMainZone = map;
+    }
+
+    public void setSourcesZone2(EnumMap<EmotivaControlCommands, String> map) {
+        sourcesZone2 = map;
+    }
+
+    public void setTunerBands(EnumMap<EmotivaControlCommands, String> map) {
+        tunerBands = map;
+    }
+
+    public void updateChannel(String channel, State state) {
+        channelStateMap.put(channel, state);
+    }
+
+    public void updateSourcesMainZone(EmotivaControlCommands command, String value) {
+        sourcesMainZone.put(command, value);
+    }
+
+    public void updateModes(EmotivaSubscriptionTags tag, String value) {
+        modes.put(tag, value);
+    }
+
+    public void removeChannel(String channel) {
+        channelStateMap.remove(channel);
+    }
+}
index 954af61a2f38e81733950aef29bf81589b39dcee..adec7e6db88b0781cfe9d840240ad13c4951580d 100644 (file)
@@ -156,9 +156,7 @@ public class EmotivaUdpReceivingService {
                 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 (Arrays.copyOfRange(answer.getData(), 0, answer.getLength() - 1).length == 0) {
                     if (isConnected()) {
                         logger.debug("Nothing received, this may happen during shutdown or some unknown error");
                     }
@@ -166,7 +164,7 @@ public class EmotivaUdpReceivingService {
                 }
                 receiveNotifyFailures = 0; // message successfully received, unset failure counter
 
-                handleReceivedData(answer, receivedData, localListener);
+                handleReceivedData(answer, localListener);
             } catch (Exception e) {
                 listenerNotifyActive = false;
 
@@ -190,12 +188,11 @@ public class EmotivaUdpReceivingService {
         }
     }
 
-    private void handleReceivedData(DatagramPacket answer, byte[] receivedData,
-            Consumer<EmotivaUdpResponse> localListener) {
+    private void handleReceivedData(DatagramPacket answer, 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);
+                logger.trace("Received data on port '{}'", answer.getPort());
                 EmotivaUdpResponse emotivaUdpResponse = new EmotivaUdpResponse(
                         new String(answer.getData(), 0, answer.getLength()), answer.getAddress().getHostAddress());
                 localListener.accept(emotivaUdpResponse);
index 5b91a6896f20033e817336dcb4e1fa443a27a46e..216d5035803707c3b5b48c7eb597e70682632207 100644 (file)
@@ -21,6 +21,7 @@ import java.net.InetAddress;
 import java.net.SocketException;
 import java.nio.charset.Charset;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
@@ -122,12 +123,11 @@ public class EmotivaUdpSendingService {
         }
     }
 
-    private void handleReceivedData(DatagramPacket answer, byte[] receivedData,
-            Consumer<EmotivaUdpResponse> localListener) {
+    private void handleReceivedData(DatagramPacket answer, 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);
+                logger.trace("Received data on port '{}'", answer.getPort());
                 EmotivaUdpResponse emotivaUdpResponse = new EmotivaUdpResponse(
                         new String(answer.getData(), 0, answer.getLength()), answer.getAddress().getHostAddress());
                 localListener.accept(emotivaUdpResponse);
@@ -158,7 +158,7 @@ public class EmotivaUdpSendingService {
         send(emotivaXmlUtils.marshallJAXBElementObjects(dto));
     }
 
-    public void sendSubscription(EmotivaSubscriptionTags[] tags, EmotivaConfiguration config) throws IOException {
+    public void sendSubscription(List<EmotivaSubscriptionTags> tags, EmotivaConfiguration config) throws IOException {
         send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaSubscriptionRequest(tags, config.protocolVersion)));
     }
 
@@ -171,7 +171,7 @@ public class EmotivaUdpSendingService {
         send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaUpdateRequest(tags, config.protocolVersion)));
     }
 
-    public void sendUnsubscribe(EmotivaSubscriptionTags[] defaultCommand) throws IOException {
+    public void sendUnsubscribe(List<EmotivaSubscriptionTags> defaultCommand) throws IOException {
         send(emotivaXmlUtils.marshallJAXBElementObjects(new EmotivaUnsubscribeDTO(defaultCommand)));
     }
 
@@ -196,14 +196,13 @@ public class EmotivaUdpSendingService {
                 logger.debug("Sending successful");
 
                 localDatagramSocket.receive(answer);
-                final byte[] receivedData = Arrays.copyOfRange(answer.getData(), 0, answer.getLength() - 1);
 
-                if (receivedData.length == 0) {
+                if (Arrays.copyOfRange(answer.getData(), 0, answer.getLength() - 1).length == 0) {
                     logger.debug("Nothing received, this may happen during shutdown or some unknown error");
                 }
 
                 if (localListener != null) {
-                    handleReceivedData(answer, receivedData, localListener);
+                    handleReceivedData(answer, localListener);
                 }
             } else {
                 throw new SocketException("Datagram Socket closed or not initialized");
index c20b2c6f5814ef2f1e9fe28404fa343e2ac6f87e..dce836a5c5853bc8483167d12bc3ae0b5148e170 100644 (file)
@@ -65,7 +65,7 @@ public class InputStateOptionProvider extends BaseDynamicStateDescriptionProvide
     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) {
+        if (typeUID == null || !BINDING_ID.equals(typeUID.getBindingId())) {
             return null;
         }
 
index faff31332029f00e8dfdbfc7d258f36a0a2f1e4e..5f1480bae7a1985025c57144b37e136b79579c04 100644 (file)
@@ -14,13 +14,12 @@ 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 java.util.stream.Collectors;
 
 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;
 
 /**
@@ -38,26 +37,18 @@ public class EmotivaSubscriptionRequest extends AbstractJAXBElementDTO {
     public EmotivaSubscriptionRequest() {
     }
 
-    public EmotivaSubscriptionRequest(List<EmotivaCommandDTO> commands, String protocol) {
+    public EmotivaSubscriptionRequest(List<EmotivaSubscriptionTags> emotivaCommandTypes, 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;
+        this.commands = emotivaCommandTypes.stream().map(EmotivaCommandDTO::fromTypeWithAck)
+                .collect(Collectors.toList());
     }
 
     public EmotivaSubscriptionRequest(EmotivaSubscriptionTags tag) {
         this.commands = List.of(EmotivaCommandDTO.fromTypeWithAck(tag));
     }
 
-    public EmotivaSubscriptionRequest(EmotivaControlCommands commandType, String protocol) {
+    public EmotivaSubscriptionRequest(EmotivaCommandDTO commandType, String protocol) {
         this.protocol = protocol;
-        this.commands = List.of(EmotivaCommandDTO.fromTypeWithAck(commandType));
+        this.commands = List.of(commandType);
     }
 }
index 6a97e53a364f0525cdbef7e558c04115cd05749c..621bcf25537d8b7639b6a1d7d189b51ed1a50508 100644 (file)
@@ -14,10 +14,10 @@ package org.openhab.binding.emotiva.internal.dto;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
-import org.openhab.binding.emotiva.internal.protocol.EmotivaControlCommands;
 import org.openhab.binding.emotiva.internal.protocol.EmotivaSubscriptionTags;
 
 /**
@@ -34,10 +34,6 @@ public class EmotivaUnsubscribeDTO extends AbstractJAXBElementDTO {
     public EmotivaUnsubscribeDTO() {
     }
 
-    public EmotivaUnsubscribeDTO(List<EmotivaCommandDTO> commands) {
-        this.commands = commands;
-    }
-
     public EmotivaUnsubscribeDTO(EmotivaSubscriptionTags[] emotivaCommandTypes) {
         List<EmotivaCommandDTO> list = new ArrayList<>();
         for (EmotivaSubscriptionTags commandType : emotivaCommandTypes) {
@@ -50,7 +46,11 @@ public class EmotivaUnsubscribeDTO extends AbstractJAXBElementDTO {
         this.commands = List.of(EmotivaCommandDTO.fromType(tag));
     }
 
-    public EmotivaUnsubscribeDTO(EmotivaControlCommands commandType) {
-        this.commands = List.of(EmotivaCommandDTO.fromType(commandType));
+    public EmotivaUnsubscribeDTO(EmotivaCommandDTO commandType) {
+        this.commands = List.of(commandType);
+    }
+
+    public EmotivaUnsubscribeDTO(List<EmotivaSubscriptionTags> commandType) {
+        this.commands = commandType.stream().map(EmotivaCommandDTO::fromTypeWithAck).collect(Collectors.toList());
     }
 }
index ab529c742b464e8e67faf59309169534ee0b4637..035b0cd5ad3b652d48d5354b3e5f0c4e5a5e9820 100644 (file)
@@ -17,13 +17,12 @@ 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.EmotivaProcessorState;
 import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.PercentType;
@@ -53,11 +52,11 @@ public class EmotivaControlRequest {
     private final EmotivaControlCommands downCommand;
     private double maxValue;
     private double minValue;
-    private final Map<String, Map<EmotivaControlCommands, String>> commandMaps;
+    private final EmotivaProcessorState state;
     private final EmotivaProtocolVersion protocolVersion;
 
     public EmotivaControlRequest(String channel, EmotivaSubscriptionTags channelSubscription,
-            EmotivaControlCommands controlCommand, Map<String, Map<EmotivaControlCommands, String>> commandMaps,
+            EmotivaControlCommands controlCommand, EmotivaProcessorState state,
             EmotivaProtocolVersion protocolVersion) {
         if (channelSubscription.equals(EmotivaSubscriptionTags.unknown)) {
             if (controlCommand.equals(EmotivaControlCommands.none)) {
@@ -94,7 +93,7 @@ public class EmotivaControlRequest {
         this.name = defaultCommand.name();
         this.dataType = defaultCommand.getDataType();
         this.channel = channel;
-        this.commandMaps = commandMaps;
+        this.state = state;
         this.protocolVersion = protocolVersion;
         if (name.equals(EmotivaControlCommands.volume.name())
                 || name.equals(EmotivaControlCommands.zone2_volume.name())) {
@@ -155,10 +154,10 @@ public class EmotivaControlRequest {
             case NONE -> {
                 switch (channel) {
                     case CHANNEL_TUNER_BAND -> {
-                        return matchToCommandMap(ohCommand, tuner_band.getEmotivaName());
+                        return matchToCommandMap(ohCommand, MAP_TUNER_BANDS);
                     }
                     case CHANNEL_TUNER_CHANNEL_SELECT -> {
-                        return matchToCommandMap(ohCommand, tuner_channel.getEmotivaName());
+                        return matchToCommandMap(ohCommand, MAP_TUNER_CHANNELS);
                     }
                     case CHANNEL_SOURCE -> {
                         return matchToCommandMap(ohCommand, MAP_SOURCES_MAIN_ZONE);
@@ -284,15 +283,13 @@ public class EmotivaControlRequest {
 
     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);
-                    }
+            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);
                 }
             }
         }
@@ -469,7 +466,7 @@ 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 + ", commandMaps=" + commandMaps
-                + ", protocolVersion=" + protocolVersion + '}';
+                + ", maxValue=" + maxValue + ", minValue=" + minValue + ", state=" + state + ", protocolVersion="
+                + protocolVersion + '}';
     }
 }
index 2e2d64809d08352dbd629c4cb1fa75e8119268b2..b4c5dce435e0bcb9dcb1566624ea39264bc7cfec 100644 (file)
@@ -132,24 +132,14 @@ public enum EmotivaSubscriptionTags {
         return EmotivaSubscriptionTags.unknown;
     }
 
-    public static EmotivaSubscriptionTags[] generalChannels() {
+    public static List<EmotivaSubscriptionTags> channels(String zonePrefix) {
         List<EmotivaSubscriptionTags> tags = new ArrayList<>();
         for (EmotivaSubscriptionTags value : values()) {
-            if (value.channel.startsWith("general")) {
+            if (value.channel.startsWith(zonePrefix)) {
                 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]);
+        return tags;
     }
 
     public static EmotivaSubscriptionTags[] speakerChannels() {
index b495509cb16d59a726fa2e9a9bf9ab63646e67a3..2c4417851f241d0fab4bf26dc257004a0cefe8ce 100644 (file)
@@ -32,8 +32,6 @@ import static org.openhab.binding.emotiva.internal.protocol.EmotivaDataType.ON_O
 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;
@@ -90,9 +88,9 @@ class EmotivaCommandHelperTest {
     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<>();
+        EmotivaProcessorState state = new EmotivaProcessorState();
 
-        EmotivaControlRequest surround = EmotivaCommandHelper.channelToControlRequest(channel, commandMaps, version);
+        EmotivaControlRequest surround = EmotivaCommandHelper.channelToControlRequest(channel, state, version);
         assertThat(surround.getName(), is(name));
         assertThat(surround.getChannel(), is(channel));
         assertThat(surround.getDataType(), is(emotivaDataType));
diff --git a/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaProcessorStateTest.java b/bundles/org.openhab.binding.emotiva/src/test/java/org/openhab/binding/emotiva/internal/EmotivaProcessorStateTest.java
new file mode 100644 (file)
index 0000000..29e904a
--- /dev/null
@@ -0,0 +1,127 @@
+/**
+ * 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.MAP_MODES;
+import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MAP_SOURCES_MAIN_ZONE;
+import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MAP_SOURCES_ZONE_2;
+import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MAP_TUNER_BANDS;
+import static org.openhab.binding.emotiva.internal.EmotivaBindingConstants.MAP_TUNER_CHANNELS;
+
+import java.util.EnumMap;
+import java.util.Optional;
+
+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.EmotivaSubscriptionTags;
+import org.openhab.core.library.types.DecimalType;
+
+/**
+ * Unit tests for the EmotivaProcessorHandlerState.
+ *
+ * @author Espen Fossen - Initial contribution
+ */
+@NonNullByDefault
+class EmotivaProcessorStateTest {
+
+    @Test
+    void initialState() {
+        var state = new EmotivaProcessorState();
+
+        assertThat(state.getSourcesMainZone(), not(nullValue()));
+        assertThat(state.getSourcesMainZone().size(), is(0));
+
+        assertThat(state.getSourcesZone2(), not(nullValue()));
+        assertThat(state.getSourcesZone2().size(), is(0));
+
+        assertThat(state.getModes(), not(nullValue()));
+        assertThat(state.getModes().size(), is(0));
+
+        assertThat(state.getChannel(EmotivaSubscriptionTags.keepAlive), is(Optional.empty()));
+
+        assertThat(state.getCommandMap(MAP_SOURCES_MAIN_ZONE).size(), is(0));
+        assertThat(state.getCommandMap(MAP_SOURCES_ZONE_2).size(), is(0));
+        assertThat(state.getCommandMap(MAP_TUNER_CHANNELS).size(), is(20));
+        assertThat(state.getCommandMap(MAP_TUNER_BANDS).size(), is(2));
+        assertThat(state.getCommandMap(MAP_MODES).size(), is(0));
+    }
+
+    @Test
+    void updateAndRemoveChannel() {
+        var state = new EmotivaProcessorState();
+
+        assertThat(state.getChannel(EmotivaSubscriptionTags.keepAlive.getChannel()), is(Optional.empty()));
+
+        state.updateChannel(EmotivaSubscriptionTags.keepAlive.getChannel(), new DecimalType(10));
+        assertThat(state.getChannel(EmotivaSubscriptionTags.keepAlive.getChannel()),
+                is(Optional.of(new DecimalType(10))));
+
+        state.removeChannel(EmotivaSubscriptionTags.keepAlive.getChannel());
+        assertThat(state.getChannel(EmotivaSubscriptionTags.keepAlive.getChannel()), is(Optional.empty()));
+    }
+
+    @Test
+    void replaceSourcesMap() {
+        var state = new EmotivaProcessorState();
+
+        assertThat(state.getSourcesMainZone(), not(nullValue()));
+        assertThat(state.getSourcesMainZone().size(), is(0));
+
+        EnumMap<EmotivaControlCommands, String> sourcesMap = new EnumMap<>(EmotivaControlCommands.class);
+        sourcesMap.put(EmotivaControlCommands.source_1, "HDMI1");
+        state.setSourcesMainZone(sourcesMap);
+
+        assertThat(state.getSourcesMainZone(), not(nullValue()));
+        assertThat(state.getSourcesMainZone().size(), is(1));
+        assertThat(state.getSourcesMainZone().get(EmotivaControlCommands.source_1), is("HDMI1"));
+    }
+
+    @Test
+    void updateModes() {
+        var state = new EmotivaProcessorState();
+
+        state.updateModes(EmotivaSubscriptionTags.mode_auto, "Auto");
+
+        assertThat(state.getModes(), not(nullValue()));
+        assertThat(state.getModes().size(), is(1));
+        assertThat(state.getModes().get(EmotivaSubscriptionTags.mode_auto), is("Auto"));
+
+        state.updateModes(EmotivaSubscriptionTags.mode_auto, "Custom Label");
+
+        assertThat(state.getModes(), not(nullValue()));
+        assertThat(state.getModes().size(), is(1));
+        assertThat(state.getModes().get(EmotivaSubscriptionTags.mode_auto), is("Custom Label"));
+    }
+
+    @Test
+    void updateSourcesMap() {
+        var state = new EmotivaProcessorState();
+
+        EnumMap<EmotivaControlCommands, String> sourcesMap = new EnumMap<>(EmotivaControlCommands.class);
+        sourcesMap.put(EmotivaControlCommands.source_1, "HDMI1");
+        state.setSourcesMainZone(sourcesMap);
+
+        assertThat(state.getSourcesMainZone(), not(nullValue()));
+        assertThat(state.getSourcesMainZone().size(), is(1));
+        assertThat(state.getSourcesMainZone().get(EmotivaControlCommands.source_1), is("HDMI1"));
+
+        state.updateSourcesMainZone(EmotivaControlCommands.source_1, "SHIELD");
+
+        assertThat(state.getSourcesMainZone(), not(nullValue()));
+        assertThat(state.getSourcesMainZone().size(), is(1));
+        assertThat(state.getSourcesMainZone().get(EmotivaControlCommands.source_1), is("SHIELD"));
+    }
+}
index 8f3c9a8b781577e8357a17b95584c638713f535f..8315eec64c00b2b5944f5028fb54e9be96494dc3 100644 (file)
@@ -49,20 +49,16 @@ class EmotivaSubscriptionRequestTest extends AbstractDTOTestBase {
     }
 
     @Test
-    void marshallWithTwoSubscriptionsNoAck() {
-        EmotivaCommandDTO command1 = new EmotivaCommandDTO(EmotivaControlCommands.volume, "10", "yes");
-        EmotivaCommandDTO command2 = new EmotivaCommandDTO(EmotivaControlCommands.power_off);
+    void marshallWithSubscriptionNoAck() {
+        EmotivaCommandDTO command = new EmotivaCommandDTO(EmotivaControlCommands.volume, "10", "yes");
 
-        EmotivaSubscriptionRequest dto = new EmotivaSubscriptionRequest(List.of(command1, command2),
-                PROTOCOL_V2.value());
+        EmotivaSubscriptionRequest dto = new EmotivaSubscriptionRequest(command, 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
index 95f4101e65ecabf51926acc3d1a5839b16dafa9f..e12c5b7570837e1791f79b0ba2999c5c664f018d 100644 (file)
@@ -16,8 +16,6 @@ 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;
@@ -50,16 +48,13 @@ class EmotivaUnsubscriptionTest extends AbstractDTOTestBase {
     @Test
     void marshallWithTwoUnsubscriptions() {
         EmotivaCommandDTO command1 = new EmotivaCommandDTO(EmotivaControlCommands.volume);
-        EmotivaCommandDTO command2 = new EmotivaCommandDTO(EmotivaControlCommands.power_off);
 
-        EmotivaUnsubscribeDTO dto = new EmotivaUnsubscribeDTO(List.of(command1, command2));
+        EmotivaUnsubscribeDTO dto = new EmotivaUnsubscribeDTO(command1);
 
         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>")));
     }
 }
index 754f18a1cd64afa271574458e31804c7a9b3af9b..51f85fb247ac10ea9fc440c9d7d60ea2f80b9031 100644 (file)
@@ -17,15 +17,12 @@ 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;
@@ -33,8 +30,8 @@ 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.EmotivaProcessorState;
 import org.openhab.binding.emotiva.internal.dto.EmotivaControlDTO;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.OnOffType;
@@ -220,7 +217,7 @@ class EmotivaControlRequestTest {
     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<>();
+    private static final EmotivaProcessorState state = new EmotivaProcessorState();
 
     @BeforeAll
     static void beforeAll() {
@@ -228,7 +225,7 @@ class EmotivaControlRequestTest {
         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);
+        state.setSourcesMainZone(MAP_SOURCES_MAIN_ZONE);
 
         MAP_SOURCES_ZONE_2.put(source_1, "HDMI 1");
         MAP_SOURCES_ZONE_2.put(source_2, "SHIELD");
@@ -236,16 +233,16 @@ class EmotivaControlRequestTest {
         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);
+        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");
-        COMMAND_MAPS.put(tuner_channel.getEmotivaName(), CHANNEL_MAP);
+        state.setChannels(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.setTunerBands(RADIO_BAND_MAP);
 
         STATE_MAP.put(CHANNEL_TREBLE, new DecimalType(-3));
         STATE_MAP.put(CHANNEL_TUNER_CHANNEL, new StringType("FM    87.50MHz"));
@@ -256,7 +253,7 @@ class EmotivaControlRequestTest {
     @MethodSource("channelToDTOs")
     void createDTO(String channel, Command ohValue, EmotivaControlCommands controlCommand,
             EmotivaProtocolVersion protocolVersion, String requestValue) {
-        EmotivaControlRequest controlRequest = EmotivaCommandHelper.channelToControlRequest(channel, COMMAND_MAPS,
+        EmotivaControlRequest controlRequest = EmotivaCommandHelper.channelToControlRequest(channel, state,
                 protocolVersion);
 
         EmotivaControlDTO dto = controlRequest.createDTO(ohValue, STATE_MAP.get(channel));