]> git.basschouten.com Git - openhab-addons.git/commitdiff
[anthem] Add channel, refactor parser, add tests (#14720)
authorMark Hilbush <mark@hilbush.com>
Thu, 4 May 2023 21:26:14 +0000 (17:26 -0400)
committerGitHub <noreply@github.com>
Thu, 4 May 2023 21:26:14 +0000 (23:26 +0200)
* Add channel and refactor parser

Signed-off-by: Mark Hilbush <mark@hilbush.com>
12 files changed:
bundles/org.openhab.binding.anthem/README.md
bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemBindingConstants.java
bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommand.java
bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParser.java
bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java
bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemUpdate.java [new file with mode: 0644]
bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/PropertyUpdate.java [new file with mode: 0644]
bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/StateUpdate.java [new file with mode: 0644]
bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/i18n/anthem.properties
bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/update/instructions.xml [new file with mode: 0644]
bundles/org.openhab.binding.anthem/src/test/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParserTest.java [new file with mode: 0644]

index 0e84fab9d2b8268d581ae3c7d852690947b2f36b..261d3cfcb33f2b1dadb827b3bef432a6b33ef2c0 100644 (file)
@@ -30,22 +30,24 @@ The Anthem AV processor supports the following channels (some zones/channels are
 
 | Channel                 | Type    | Description  |
 |-------------------------|---------|--------------|
-| *Main Zone*             |         |   |
-| 1#power                 | Switch  | Power the zone on or off  |
-| 1#volume                | Dimmer  | Increase or decrease the volume level  |
-| 1#volumeDB              | Number  | The actual volume setting  |
-| 1#mute                  | Switch  | Mute the volume  |
-| 1#activeInput           | Number  | The currently active input source  |
+| *General*               |         |                                          |
+| general#command         | String  | Send a custom command                    |
+| *Main Zone*             |         |                                          |
+| 1#power                 | Switch  | Power the zone on or off                 |
+| 1#volume                | Dimmer  | Increase or decrease the volume level    |
+| 1#volumeDB              | Number  | The actual volume setting                |
+| 1#mute                  | Switch  | Mute the volume                          |
+| 1#activeInput           | Number  | The currently active input source        |
 | 1#activeInputShortName  | String  | Short friendly name of the active input  |
-| 1#activeInputLongName   | String  | Long friendly name of the active input |
-| *Zone 2*                |         |   |
-| 2#power                 | Switch  | Power the zone on or off  |
-| 2#volume                | Dimmer  | Increase or decrease the volume level  |
-| 2#volumeDB              | Number  | The actual volume setting  |
-| 2#mute                  | Switch  | Mute the volume  |
-| 2#activeInput           | Number  | The currently active input source  |
+| 1#activeInputLongName   | String  | Long friendly name of the active input   |
+| *Zone 2*                |         |                                          |
+| 2#power                 | Switch  | Power the zone on or off                 |
+| 2#volume                | Dimmer  | Increase or decrease the volume level    |
+| 2#volumeDB              | Number  | The actual volume setting                |
+| 2#mute                  | Switch  | Mute the volume                          |
+| 2#activeInput           | Number  | The currently active input source        |
 | 2#activeInputShortName  | String  | Short friendly name of the active input  |
-| 2#activeInputLongName   | String  | Long friendly name of the active input |
+| 2#activeInputLongName   | String  | Long friendly name of the active input   |
 
 
 ## Full Example
@@ -59,6 +61,8 @@ Thing anthem:anthem:mediaroom "Anthem AVM 60" [ host="192.168.1.100" ]
 ### Items
 
 ```
+String  Anthem_Command                    "Command [%s]"                           { channel="anthem:anthem:mediaroom:general#command" }
+
 Switch  Anthem_Z1_Power                   "Zone 1 Power [%s]"                      { channel="anthem:anthem:mediaroom:1#power" }
 Dimmer  Anthem_Z1_Volume                  "Zone 1 Volume [%s]"                     { channel="anthem:anthem:mediaroom:1#volume" }
 Number  Anthem_Z1_Volume_DB               "Zone 1 Volume dB [%.0f]"                { channel="anthem:anthem:mediaroom:1#volumeDB" }
index a020f485f70c7e05a2f45daa75f2b471e6bac37e..e697c5021b9c7c915e1debf56c45191e04d057b6 100644 (file)
@@ -32,6 +32,9 @@ public class AnthemBindingConstants {
     // List of all Thing Type UIDs
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ANTHEM);
 
+    // Channel groups
+    public static final String CHANNEL_GROUP_GENERAL = "general";
+
     // Channel Ids
     public static final String CHANNEL_POWER = "power";
     public static final String CHANNEL_VOLUME = "volume";
@@ -40,6 +43,7 @@ public class AnthemBindingConstants {
     public static final String CHANNEL_ACTIVE_INPUT = "activeInput";
     public static final String CHANNEL_ACTIVE_INPUT_SHORT_NAME = "activeInputShortName";
     public static final String CHANNEL_ACTIVE_INPUT_LONG_NAME = "activeInputLongName";
+    public static final String CHANNEL_COMMAND = "command";
 
     // Connection-related configuration parameters
     public static final int DEFAULT_PORT = 14999;
@@ -47,4 +51,8 @@ public class AnthemBindingConstants {
     public static final int DEFAULT_COMMAND_DELAY_MSEC = 100;
 
     public static final char COMMAND_TERMINATION_CHAR = ';';
+
+    public static final String PROPERTY_REGION = "region";
+    public static final String PROPERTY_SOFTWARE_BUILD_DATE = "softwareBuildDate";
+    public static final String PROPERTY_NUM_AVAILABLE_INPUTS = "numAvailableInputs";
 }
index ed1797912115d622f64766ac1ea834d2b70d37f8..c05333961f3d4784f0b3ea69f1bd7c732cfbbab6 100644 (file)
@@ -116,6 +116,10 @@ public class AnthemCommand {
         return new AnthemCommand("IDN?");
     }
 
+    public static AnthemCommand customCommand(String customCommand) {
+        return new AnthemCommand(customCommand);
+    }
+
     public String getCommand() {
         return command + COMMAND_TERMINATOR;
     }
index 8349093013b226433466663c91c732b3a44d9613..c40e56313c16785f50beb42256603aabee572d59 100644 (file)
@@ -23,7 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Thing;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory;
  */
 @NonNullByDefault
 public class AnthemCommandParser {
-    private static final Pattern NUM_AVAILABLE_INPUTS_PATTERN = Pattern.compile("ICN([0-9])");
+    private static final Pattern NUM_AVAILABLE_INPUTS_PATTERN = Pattern.compile("ICN([0-9]{1,2})");
     private static final Pattern INPUT_SHORT_NAME_PATTERN = Pattern.compile("ISN([0-9][0-9])(\\p{ASCII}*)");
     private static final Pattern INPUT_LONG_NAME_PATTERN = Pattern.compile("ILN([0-9][0-9])(\\p{ASCII}*)");
     private static final Pattern POWER_PATTERN = Pattern.compile("Z([0-9])POW([01])");
@@ -45,39 +45,27 @@ public class AnthemCommandParser {
 
     private Logger logger = LoggerFactory.getLogger(AnthemCommandParser.class);
 
-    private AnthemHandler handler;
+    private Map<String, String> inputShortNamesMap = new HashMap<>();
+    private Map<String, String> inputLongNamesMap = new HashMap<>();
 
-    private Map<Integer, String> inputShortNamesMap = new HashMap<>();
-    private Map<Integer, String> inputLongNamesMap = new HashMap<>();
-
-    private int numAvailableInputs;
-
-    public AnthemCommandParser(AnthemHandler anthemHandler) {
-        this.handler = anthemHandler;
-    }
-
-    public int getNumAvailableInputs() {
-        return numAvailableInputs;
-    }
-
-    public void parseMessage(String command) {
+    public @Nullable AnthemUpdate parseCommand(String command) {
         if (!isValidCommand(command)) {
-            return;
+            return null;
         }
         // Strip off the termination char and any whitespace
         String cmd = command.substring(0, command.indexOf(COMMAND_TERMINATION_CHAR)).trim();
 
         // Zone command
         if (cmd.startsWith("Z")) {
-            parseZoneCommand(cmd);
+            return parseZoneCommand(cmd);
         }
         // Information command
         else if (cmd.startsWith("ID")) {
-            parseInformationCommand(cmd);
+            return parseInformationCommand(cmd);
         }
         // Number of inputs
         else if (cmd.startsWith("ICN")) {
-            parseNumberOfAvailableInputsCommand(cmd);
+            return parseNumberOfAvailableInputsCommand(cmd);
         }
         // Input short name
         else if (cmd.startsWith("ISN")) {
@@ -95,6 +83,15 @@ public class AnthemCommandParser {
         else {
             logger.trace("Command parser doesn't know how to handle command: '{}'", cmd);
         }
+        return null;
+    }
+
+    public @Nullable String getInputShortName(String input) {
+        return inputShortNamesMap.get(input);
+    }
+
+    public @Nullable String getInputLongName(String input) {
+        return inputLongNamesMap.get(input);
     }
 
     private boolean isValidCommand(String command) {
@@ -106,45 +103,47 @@ public class AnthemCommandParser {
         return true;
     }
 
-    private void parseZoneCommand(String command) {
+    private @Nullable AnthemUpdate parseZoneCommand(String command) {
         // Power update
         if (command.contains("POW")) {
-            parsePower(command);
+            return parsePower(command);
         }
         // Volume update
         else if (command.contains("VOL")) {
-            parseVolume(command);
+            return parseVolume(command);
         }
         // Mute update
         else if (command.contains("MUT")) {
-            parseMute(command);
+            return parseMute(command);
         }
         // Active input
         else if (command.contains("INP")) {
-            parseActiveInput(command);
+            return parseActiveInput(command);
         }
+        return null;
     }
 
-    private void parseInformationCommand(String command) {
+    private @Nullable AnthemUpdate parseInformationCommand(String command) {
         String value = command.substring(3, command.length());
+        AnthemUpdate update = null;
         switch (command.substring(2, 3)) {
             case "M":
-                handler.setModel(value);
+                update = AnthemUpdate.createPropertyUpdate(Thing.PROPERTY_MODEL_ID, value);
                 break;
             case "R":
-                handler.setRegion(value);
+                update = AnthemUpdate.createPropertyUpdate(PROPERTY_REGION, value);
                 break;
             case "S":
-                handler.setSoftwareVersion(value);
+                update = AnthemUpdate.createPropertyUpdate(Thing.PROPERTY_FIRMWARE_VERSION, value);
                 break;
             case "B":
-                handler.setSoftwareBuildDate(value);
+                update = AnthemUpdate.createPropertyUpdate(PROPERTY_SOFTWARE_BUILD_DATE, value);
                 break;
             case "H":
-                handler.setHardwareVersion(value);
+                update = AnthemUpdate.createPropertyUpdate(Thing.PROPERTY_HARDWARE_VERSION, value);
                 break;
             case "N":
-                handler.setMacAddress(value);
+                update = AnthemUpdate.createPropertyUpdate(Thing.PROPERTY_MAC_ADDRESS, value);
                 break;
             case "Q":
                 // Ignore
@@ -153,21 +152,20 @@ public class AnthemCommandParser {
                 logger.debug("Unknown info type");
                 break;
         }
+        return update;
     }
 
-    private void parseNumberOfAvailableInputsCommand(String command) {
+    private @Nullable AnthemUpdate parseNumberOfAvailableInputsCommand(String command) {
         Matcher matcher = NUM_AVAILABLE_INPUTS_PATTERN.matcher(command);
         if (matcher != null) {
             try {
                 matcher.find();
-                String numAvailableInputsStr = matcher.group(1);
-                DecimalType numAvailableInputs = DecimalType.valueOf(numAvailableInputsStr);
-                handler.setNumAvailableInputs(numAvailableInputs.intValue());
-                this.numAvailableInputs = numAvailableInputs.intValue();
-            } catch (NumberFormatException | IndexOutOfBoundsException | IllegalStateException e) {
+                return AnthemUpdate.createPropertyUpdate(PROPERTY_NUM_AVAILABLE_INPUTS, matcher.group(1));
+            } catch (IndexOutOfBoundsException | IllegalStateException e) {
                 logger.debug("Parsing exception on command: {}", command, e);
             }
         }
+        return null;
     }
 
     private void parseInputShortNameCommand(String command) {
@@ -182,11 +180,11 @@ public class AnthemCommandParser {
         logger.info("Command was not processed successfully by the device: '{}'", command);
     }
 
-    private void parseInputName(String command, @Nullable Matcher matcher, Map<Integer, String> map) {
+    private void parseInputName(String command, @Nullable Matcher matcher, Map<String, String> map) {
         if (matcher != null) {
             try {
                 matcher.find();
-                int input = Integer.parseInt(matcher.group(1));
+                String input = matcher.group(1);
                 String inputName = matcher.group(2);
                 map.putIfAbsent(input, inputName);
             } catch (NumberFormatException | IndexOutOfBoundsException | IllegalStateException e) {
@@ -195,69 +193,65 @@ public class AnthemCommandParser {
         }
     }
 
-    private void parsePower(String command) {
+    private @Nullable AnthemUpdate parsePower(String command) {
         Matcher mmatcher = POWER_PATTERN.matcher(command);
         if (mmatcher != null) {
             try {
                 mmatcher.find();
                 String zone = mmatcher.group(1);
                 String power = mmatcher.group(2);
-                handler.updateChannelState(zone, CHANNEL_POWER, "1".equals(power) ? OnOffType.ON : OnOffType.OFF);
-                handler.checkPowerStatusChange(zone, power);
+                return AnthemUpdate.createStateUpdate(zone, CHANNEL_POWER,
+                        "1".equals(power) ? OnOffType.ON : OnOffType.OFF);
             } catch (IndexOutOfBoundsException | IllegalStateException e) {
                 logger.debug("Parsing exception on command: {}", command, e);
             }
         }
+        return null;
     }
 
-    private void parseVolume(String command) {
+    private @Nullable AnthemUpdate parseVolume(String command) {
         Matcher matcher = VOLUME_PATTERN.matcher(command);
         if (matcher != null) {
             try {
                 matcher.find();
                 String zone = matcher.group(1);
                 String volume = matcher.group(2);
-                handler.updateChannelState(zone, CHANNEL_VOLUME_DB, DecimalType.valueOf(volume));
+                return AnthemUpdate.createStateUpdate(zone, CHANNEL_VOLUME_DB, DecimalType.valueOf(volume));
             } catch (NumberFormatException | IndexOutOfBoundsException | IllegalStateException e) {
                 logger.debug("Parsing exception on command: {}", command, e);
             }
         }
+        return null;
     }
 
-    private void parseMute(String command) {
+    private @Nullable AnthemUpdate parseMute(String command) {
         Matcher matcher = MUTE_PATTERN.matcher(command);
         if (matcher != null) {
             try {
                 matcher.find();
                 String zone = matcher.group(1);
                 String mute = matcher.group(2);
-                handler.updateChannelState(zone, CHANNEL_MUTE, "1".equals(mute) ? OnOffType.ON : OnOffType.OFF);
+                return AnthemUpdate.createStateUpdate(zone, CHANNEL_MUTE,
+                        "1".equals(mute) ? OnOffType.ON : OnOffType.OFF);
             } catch (IndexOutOfBoundsException | IllegalStateException e) {
                 logger.debug("Parsing exception on command: {}", command, e);
             }
         }
+        return null;
     }
 
-    private void parseActiveInput(String command) {
+    private @Nullable AnthemUpdate parseActiveInput(String command) {
         Matcher matcher = ACTIVE_INPUT_PATTERN.matcher(command);
         if (matcher != null) {
             try {
                 matcher.find();
                 String zone = matcher.group(1);
                 DecimalType activeInput = DecimalType.valueOf(matcher.group(2));
-                handler.updateChannelState(zone, CHANNEL_ACTIVE_INPUT, activeInput);
-                String name;
-                name = inputShortNamesMap.get(activeInput.intValue());
-                if (name != null) {
-                    handler.updateChannelState(zone, CHANNEL_ACTIVE_INPUT_SHORT_NAME, new StringType(name));
-                }
-                name = inputShortNamesMap.get(activeInput.intValue());
-                if (name != null) {
-                    handler.updateChannelState(zone, CHANNEL_ACTIVE_INPUT_LONG_NAME, new StringType(name));
-                }
+                return AnthemUpdate.createStateUpdate(zone, CHANNEL_ACTIVE_INPUT, activeInput);
             } catch (NumberFormatException | IndexOutOfBoundsException | IllegalStateException e) {
                 logger.debug("Parsing exception on command: {}", command, e);
             }
         }
+        return null;
     }
 }
index 3f66d753e335c5445c854d85445ae4fae5775255..2e7448c93b0c0ea1196f32f4b3bcd16736f07dab 100644 (file)
@@ -35,6 +35,7 @@ import org.openhab.binding.anthem.internal.AnthemConfiguration;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.IncreaseDecreaseType;
 import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
@@ -64,7 +65,7 @@ public class AnthemHandler extends BaseThingHandler {
     private @Nullable BufferedWriter writer;
     private @Nullable BufferedReader reader;
 
-    private AnthemCommandParser messageParser;
+    private AnthemCommandParser commandParser;
 
     private final BlockingQueue<AnthemCommand> sendQueue = new LinkedBlockingQueue<>();
 
@@ -83,7 +84,7 @@ public class AnthemHandler extends BaseThingHandler {
 
     public AnthemHandler(Thing thing) {
         super(thing);
-        messageParser = new AnthemCommandParser(this);
+        commandParser = new AnthemCommandParser();
     }
 
     @Override
@@ -120,6 +121,28 @@ public class AnthemHandler extends BaseThingHandler {
         if (groupId == null) {
             return;
         }
+
+        if (CHANNEL_GROUP_GENERAL.equals(groupId)) {
+            handleGeneralCommand(channelUID, command);
+        } else {
+            handleZoneCommand(groupId, channelUID, command);
+        }
+    }
+
+    private void handleGeneralCommand(ChannelUID channelUID, Command command) {
+        switch (channelUID.getIdWithoutGroup()) {
+            case CHANNEL_COMMAND:
+                if (command instanceof StringType) {
+                    sendCommand(AnthemCommand.customCommand(command.toString()));
+                }
+                break;
+            default:
+                logger.debug("Received general command '{}' for unhandled channel '{}'", command, channelUID.getId());
+                break;
+        }
+    }
+
+    private void handleZoneCommand(String groupId, ChannelUID channelUID, Command command) {
         Zone zone = Zone.fromValue(groupId);
 
         switch (channelUID.getIdWithoutGroup()) {
@@ -162,71 +185,11 @@ public class AnthemHandler extends BaseThingHandler {
                 }
                 break;
             default:
-                logger.debug("Received command '{}' for unhandled channel '{}'", command, channelUID.getId());
+                logger.debug("Received zone command '{}' for unhandled channel '{}'", command, channelUID.getId());
                 break;
         }
     }
 
-    public void setModel(String model) {
-        updateProperty("Model", model);
-    }
-
-    public void setRegion(String region) {
-        updateProperty("Region", region);
-    }
-
-    public void setSoftwareVersion(String version) {
-        updateProperty("Software Version", version);
-    }
-
-    public void setSoftwareBuildDate(String date) {
-        updateProperty("Software Build Date", date);
-    }
-
-    public void setHardwareVersion(String version) {
-        updateProperty("Hardware Version", version);
-    }
-
-    public void setMacAddress(String mac) {
-        updateProperty("Mac Address", mac);
-    }
-
-    public void updateChannelState(String zone, String channelId, State state) {
-        updateState(zone + "#" + channelId, state);
-    }
-
-    public void checkPowerStatusChange(String zone, String power) {
-        // Zone 1
-        if (Zone.MAIN.equals(Zone.fromValue(zone))) {
-            boolean newZone1PowerState = "1".equals(power) ? true : false;
-            if (!zone1PreviousPowerState && newZone1PowerState) {
-                // Power turned on for main zone.
-                // This will cause the main zone channel states to be updated
-                scheduler.submit(() -> queryAdditionalInformation(Zone.MAIN));
-            }
-            zone1PreviousPowerState = newZone1PowerState;
-        }
-        // Zone 2
-        else if (Zone.ZONE2.equals(Zone.fromValue(zone))) {
-            boolean newZone2PowerState = "1".equals(power) ? true : false;
-            if (!zone2PreviousPowerState && newZone2PowerState) {
-                // Power turned on for zone 2.
-                // This will cause zone 2 channel states to be updated
-                scheduler.submit(() -> queryAdditionalInformation(Zone.ZONE2));
-            }
-            zone2PreviousPowerState = newZone2PowerState;
-        }
-    }
-
-    public void setNumAvailableInputs(int numInputs) {
-        // Request the names for all the inputs
-        for (int input = 1; input <= numInputs; input++) {
-            sendCommand(AnthemCommand.queryInputShortName(input));
-            sendCommand(AnthemCommand.queryInputLongName(input));
-        }
-        updateProperty("Number of Inputs", String.valueOf(numInputs));
-    }
-
     private void queryAdditionalInformation(Zone zone) {
         // Request information about the device
         sendCommand(AnthemCommand.queryNumAvailableInputs());
@@ -418,7 +381,10 @@ public class AnthemHandler extends BaseThingHandler {
                 if (c == COMMAND_TERMINATION_CHAR) {
                     command = sbReader.toString();
                     logger.debug("Reader thread sending command to parser: {}", command);
-                    messageParser.parseMessage(command);
+                    AnthemUpdate update = commandParser.parseCommand(command);
+                    if (update != null) {
+                        processUpdate(update);
+                    }
                     sbReader.setLength(0);
                 }
             }
@@ -434,4 +400,88 @@ public class AnthemHandler extends BaseThingHandler {
             logger.debug("Reader thread exiting");
         }
     }
+
+    private void processUpdate(AnthemUpdate update) {
+        // State update
+        if (update.isStateUpdate()) {
+            StateUpdate stateUpdate = update.getStateUpdate();
+            updateState(stateUpdate.getGroupId() + ChannelUID.CHANNEL_GROUP_SEPARATOR + stateUpdate.getChannelId(),
+                    stateUpdate.getState());
+            postProcess(stateUpdate);
+        }
+        // Property update
+        else if (update.isPropertyUpdate()) {
+            PropertyUpdate propertyUpdate = update.getPropertyUpdate();
+            updateProperty(propertyUpdate.getName(), propertyUpdate.getValue());
+            postProcess(propertyUpdate);
+        }
+    }
+
+    private void postProcess(StateUpdate stateUpdate) {
+        switch (stateUpdate.getChannelId()) {
+            case CHANNEL_POWER:
+                checkPowerStatusChange(stateUpdate);
+                break;
+            case CHANNEL_ACTIVE_INPUT:
+                updateInputNameChannels(stateUpdate);
+                break;
+        }
+    }
+
+    private void checkPowerStatusChange(StateUpdate stateUpdate) {
+        String zone = stateUpdate.getGroupId();
+        State power = stateUpdate.getState();
+        // Zone 1
+        if (Zone.MAIN.equals(Zone.fromValue(zone))) {
+            boolean newZone1PowerState = (power instanceof OnOffType && power == OnOffType.ON) ? true : false;
+            if (!zone1PreviousPowerState && newZone1PowerState) {
+                // Power turned on for main zone.
+                // This will cause the main zone channel states to be updated
+                scheduler.submit(() -> queryAdditionalInformation(Zone.MAIN));
+            }
+            zone1PreviousPowerState = newZone1PowerState;
+        }
+        // Zone 2
+        else if (Zone.ZONE2.equals(Zone.fromValue(zone))) {
+            boolean newZone2PowerState = (power instanceof OnOffType && power == OnOffType.ON) ? true : false;
+            if (!zone2PreviousPowerState && newZone2PowerState) {
+                // Power turned on for zone 2.
+                // This will cause zone 2 channel states to be updated
+                scheduler.submit(() -> queryAdditionalInformation(Zone.ZONE2));
+            }
+            zone2PreviousPowerState = newZone2PowerState;
+        }
+    }
+
+    private void updateInputNameChannels(StateUpdate stateUpdate) {
+        State state = stateUpdate.getState();
+        String groupId = stateUpdate.getGroupId();
+        if (state instanceof StringType) {
+            updateState(groupId + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ACTIVE_INPUT_SHORT_NAME,
+                    new StringType(commandParser.getInputShortName(state.toString())));
+            updateState(groupId + ChannelUID.CHANNEL_GROUP_SEPARATOR + CHANNEL_ACTIVE_INPUT_LONG_NAME,
+                    new StringType(commandParser.getInputLongName(state.toString())));
+        }
+    }
+
+    private void postProcess(PropertyUpdate propertyUpdate) {
+        switch (propertyUpdate.getName()) {
+            case PROPERTY_NUM_AVAILABLE_INPUTS:
+                queryAllInputNames(propertyUpdate);
+                break;
+        }
+    }
+
+    private void queryAllInputNames(PropertyUpdate propertyUpdate) {
+        try {
+            int numInputs = Integer.parseInt(propertyUpdate.getValue());
+            for (int input = 1; input <= numInputs; input++) {
+                sendCommand(AnthemCommand.queryInputShortName(input));
+                sendCommand(AnthemCommand.queryInputLongName(input));
+            }
+        } catch (NumberFormatException e) {
+            logger.debug("Unable to convert property '{}' to integer: {}", propertyUpdate.getName(),
+                    propertyUpdate.getValue());
+        }
+    }
 }
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemUpdate.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemUpdate.java
new file mode 100644 (file)
index 0000000..cf87880
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2023 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.anthem.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link AnthemUpdate} class represents the result of parsing the response from
+ * an Anthem processor.
+ *
+ * @author Mark Hilbush - Initial contribution
+ */
+@NonNullByDefault
+public class AnthemUpdate {
+    private Object updateObject;
+
+    public AnthemUpdate(StateUpdate stateUpdate) {
+        this.updateObject = stateUpdate;
+    }
+
+    public AnthemUpdate(PropertyUpdate propertyUpdate) {
+        this.updateObject = propertyUpdate;
+    }
+
+    public static AnthemUpdate createStateUpdate(String groupId, String channelId, State state) {
+        return new AnthemUpdate(new StateUpdate(groupId, channelId, state));
+    }
+
+    public static AnthemUpdate createPropertyUpdate(String name, String value) {
+        return new AnthemUpdate(new PropertyUpdate(name, value));
+    }
+
+    public boolean isStateUpdate() {
+        return updateObject instanceof StateUpdate;
+    }
+
+    public boolean isPropertyUpdate() {
+        return updateObject instanceof PropertyUpdate;
+    }
+
+    public StateUpdate getStateUpdate() {
+        if (updateObject instanceof StateUpdate stateUpdate) {
+            return stateUpdate;
+        }
+        throw new IllegalStateException("Update object is not a state update");
+    }
+
+    public PropertyUpdate getPropertyUpdate() {
+        if (updateObject instanceof PropertyUpdate propertyUpdate) {
+            return propertyUpdate;
+        }
+        throw new IllegalStateException("Update object is not a property update");
+    }
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/PropertyUpdate.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/PropertyUpdate.java
new file mode 100644 (file)
index 0000000..489b78c
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2023 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.anthem.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PropertyUpdate} class represents a property that need to be set
+ * or updated on the Anthem thing.
+ *
+ * @author Mark Hilbush - Initial contribution
+ */
+@NonNullByDefault
+public class PropertyUpdate {
+    private String name;
+    private String value;
+
+    public PropertyUpdate(String name, String value) {
+        this.name = name;
+        this.value = value;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getValue() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/StateUpdate.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/StateUpdate.java
new file mode 100644 (file)
index 0000000..aef3f72
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2023 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.anthem.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link StateUpdate} class represents a state that needs to be updated
+ * on an Anthem thing channel.
+ *
+ * @author Mark Hilbush - Initial contribution
+ */
+@NonNullByDefault
+public class StateUpdate {
+    private String groupId;
+    private String channelId;
+    private State state;
+
+    public StateUpdate(String groupId, String channelId, State state) {
+        this.groupId = groupId;
+        this.channelId = channelId;
+        this.state = state;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public String getChannelId() {
+        return channelId;
+    }
+
+    public State getState() {
+        return state;
+    }
+}
index fb89150d1939ab1e67f705bc1fe9143c9c49ea22..c89b89d97e6cf613940fd95c1a7f39bc0d9fb0db 100644 (file)
@@ -25,6 +25,8 @@ thing-type.config.anthem.anthem.reconnectIntervalMinutes.description = The time
 
 # channel group types
 
+channel-group-type.anthem.general.label = General Control
+channel-group-type.anthem.general.description = General channels for this AVR
 channel-group-type.anthem.zone.label = Zone Control
 channel-group-type.anthem.zone.description = Channels for a zone of this processor
 
@@ -36,6 +38,8 @@ channel-type.anthem.activeInputLongName.label = Active Input Long Name
 channel-type.anthem.activeInputLongName.description = Long friendly name of the active input source
 channel-type.anthem.activeInputShortName.label = Active Input Short Name
 channel-type.anthem.activeInputShortName.description = Short friendly name of the active input source
+channel-type.anthem.command.label = Command
+channel-type.anthem.command.description = Send a custom command to the processor
 channel-type.anthem.volumeDB.label = Volume dB
 channel-type.anthem.volumeDB.description = Set the volume level dB between -90 and 0
 
index 33985294ba6507d6bcc777c68bd8639ca0f5235e..07cac9ed1504a8ceeca369e554d16bc259986d31 100644 (file)
@@ -9,6 +9,7 @@
                <description>Thing for Anthem AV processor</description>
 
                <channel-groups>
+                       <channel-group id="general" typeId="general"/>
                        <channel-group id="1" typeId="zone">
                                <label>Main Zone</label>
                                <description>Controls zone 1 (the main zone) of the processor</description>
                </config-description>
        </thing-type>
 
+       <channel-group-type id="general">
+               <label>General Control</label>
+               <description>General channels for this AVR</description>
+               <channels>
+                       <channel id="command" typeId="command"/>
+               </channels>
+       </channel-group-type>
+
        <channel-group-type id="zone">
                <label>Zone Control</label>
                <description>Channels for a zone of this processor</description>
                <state readOnly="true"></state>
        </channel-type>
 
+       <channel-type id="command">
+               <item-type>String</item-type>
+               <label>Command</label>
+               <description>Send a custom command to the processor</description>
+               <state readOnly="false"></state>
+       </channel-type>
+
 </thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/update/instructions.xml
new file mode 100644 (file)
index 0000000..fb7d98c
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
+
+       <thing-type uid="anthem:anthem">
+
+               <instruction-set targetVersion="1">
+                       <add-channel id="command" groupIds="general">
+                               <type>anthem:command</type>
+                       </add-channel>
+               </instruction-set>
+
+       </thing-type>
+
+</update:update-descriptions>
diff --git a/bundles/org.openhab.binding.anthem/src/test/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParserTest.java b/bundles/org.openhab.binding.anthem/src/test/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParserTest.java
new file mode 100644 (file)
index 0000000..ea185c5
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2010-2023 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.anthem.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.anthem.internal.AnthemBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.Thing;
+
+/**
+ * The {@link AnthemCommandParserTest} is responsible for testing the functionality
+ * of the Anthem command parser.
+ *
+ * @author Mark Hilbush - Initial contribution
+ */
+@NonNullByDefault
+public class AnthemCommandParserTest {
+
+    AnthemCommandParser parser = new AnthemCommandParser();
+
+    @Test
+    public void testInvalidCommands() {
+        @Nullable
+        AnthemUpdate update;
+
+        update = parser.parseCommand("BOGUS_COMMAND;");
+        assertEquals(null, update);
+
+        update = parser.parseCommand("UNTERMINATED_COMMAND");
+        assertEquals(null, update);
+
+        update = parser.parseCommand("Z1POW0");
+        assertEquals(null, update);
+
+        update = parser.parseCommand("X");
+        assertEquals(null, update);
+
+        update = parser.parseCommand("Y;");
+        assertEquals(null, update);
+
+        update = parser.parseCommand("Z1POW67;");
+        assertEquals(null, update);
+
+        update = parser.parseCommand("POW0;");
+        assertEquals(null, update);
+    }
+
+    @Test
+    public void testPowerCommands() {
+        @Nullable
+        AnthemUpdate update;
+
+        update = parser.parseCommand("Z1POW1;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertTrue(update.isStateUpdate());
+            assertFalse(update.isPropertyUpdate());
+            assertEquals("1", update.getStateUpdate().getGroupId());
+            assertEquals(CHANNEL_POWER, update.getStateUpdate().getChannelId());
+            assertEquals(OnOffType.ON, update.getStateUpdate().getState());
+        }
+
+        update = parser.parseCommand("Z2POW0;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertEquals("2", update.getStateUpdate().getGroupId());
+            assertEquals(CHANNEL_POWER, update.getStateUpdate().getChannelId());
+            assertEquals(OnOffType.OFF, update.getStateUpdate().getState());
+        }
+    }
+
+    @Test
+    public void testVolumeCommands() {
+        @Nullable
+        AnthemUpdate update;
+
+        update = parser.parseCommand("Z1VOL55;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertEquals("1", update.getStateUpdate().getGroupId());
+            assertEquals(CHANNEL_VOLUME_DB, update.getStateUpdate().getChannelId());
+            assertEquals(new DecimalType(55), update.getStateUpdate().getState());
+        }
+
+        update = parser.parseCommand("Z2VOL99;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertEquals("2", update.getStateUpdate().getGroupId());
+            assertEquals(CHANNEL_VOLUME_DB, update.getStateUpdate().getChannelId());
+            assertEquals(new DecimalType(99), update.getStateUpdate().getState());
+        }
+    }
+
+    @Test
+    public void testMuteCommands() {
+        @Nullable
+        AnthemUpdate update;
+
+        update = parser.parseCommand("Z1MUT1;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertEquals("1", update.getStateUpdate().getGroupId());
+            assertEquals(CHANNEL_MUTE, update.getStateUpdate().getChannelId());
+            assertEquals(OnOffType.ON, update.getStateUpdate().getState());
+        }
+
+        update = parser.parseCommand("Z2MUT0;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertTrue(update.isStateUpdate());
+            assertEquals("2", update.getStateUpdate().getGroupId());
+            assertEquals(CHANNEL_MUTE, update.getStateUpdate().getChannelId());
+            assertEquals(OnOffType.OFF, update.getStateUpdate().getState());
+        }
+    }
+
+    @Test
+    public void testNumInputsCommand() {
+        @Nullable
+        AnthemUpdate update;
+
+        update = parser.parseCommand("ICN8;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertTrue(update.isPropertyUpdate());
+            assertEquals(PROPERTY_NUM_AVAILABLE_INPUTS, update.getPropertyUpdate().getName());
+            assertEquals("8", update.getPropertyUpdate().getValue());
+        }
+
+        update = parser.parseCommand("ICN15;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertTrue(update.isPropertyUpdate());
+            assertEquals(PROPERTY_NUM_AVAILABLE_INPUTS, update.getPropertyUpdate().getName());
+            assertEquals("15", update.getPropertyUpdate().getValue());
+        }
+    }
+
+    @Test
+    public void testRegionProperty() {
+        @Nullable
+        AnthemUpdate update;
+
+        update = parser.parseCommand("IDRUS;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertTrue(update.isPropertyUpdate());
+            assertFalse(update.isStateUpdate());
+            assertEquals(PROPERTY_REGION, update.getPropertyUpdate().getName());
+            assertEquals("US", update.getPropertyUpdate().getValue());
+        }
+    }
+
+    @Test
+    public void testSoftwareVersionProperty() {
+        @Nullable
+        AnthemUpdate update;
+
+        update = parser.parseCommand("IDS1.2.3.4;");
+        assertNotEquals(null, update);
+        if (update != null) {
+            assertTrue(update.isPropertyUpdate());
+            assertEquals(Thing.PROPERTY_FIRMWARE_VERSION, update.getPropertyUpdate().getName());
+            assertEquals("1.2.3.4", update.getPropertyUpdate().getValue());
+        }
+    }
+}