diff --git a/bundles/org.openhab.binding.anthem/README.md b/bundles/org.openhab.binding.anthem/README.md index 0e84fab9d..261d3cfcb 100644 --- a/bundles/org.openhab.binding.anthem/README.md +++ b/bundles/org.openhab.binding.anthem/README.md @@ -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" } diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemBindingConstants.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemBindingConstants.java index a020f485f..e697c5021 100644 --- a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemBindingConstants.java +++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemBindingConstants.java @@ -32,6 +32,9 @@ public class AnthemBindingConstants { // List of all Thing Type UIDs public static final Set 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"; } diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommand.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommand.java index ed1797912..c05333961 100644 --- a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommand.java +++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommand.java @@ -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; } diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParser.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParser.java index 834909301..c40e56313 100644 --- a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParser.java +++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParser.java @@ -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 inputShortNamesMap = new HashMap<>(); + private Map inputLongNamesMap = new HashMap<>(); - private Map inputShortNamesMap = new HashMap<>(); - private Map 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 map) { + private void parseInputName(String command, @Nullable Matcher matcher, Map 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; } } diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java index 3f66d753e..2e7448c93 100644 --- a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java +++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java @@ -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 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 index 000000000..cf878804b --- /dev/null +++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemUpdate.java @@ -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 index 000000000..489b78c26 --- /dev/null +++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/PropertyUpdate.java @@ -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 index 000000000..aef3f7264 --- /dev/null +++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/StateUpdate.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/i18n/anthem.properties b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/i18n/anthem.properties index fb89150d1..c89b89d97 100644 --- a/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/i18n/anthem.properties +++ b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/i18n/anthem.properties @@ -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 diff --git a/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/thing/thing-types.xml index 33985294b..07cac9ed1 100644 --- a/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/thing/thing-types.xml @@ -9,6 +9,7 @@ Thing for Anthem AV processor + Controls zone 1 (the main zone) of the processor @@ -50,6 +51,14 @@ + + + General channels for this AVR + + + + + Channels for a zone of this processor @@ -93,4 +102,11 @@ + + String + + Send a custom command to the processor + + + 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 index 000000000..fb7d98cef --- /dev/null +++ b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/update/instructions.xml @@ -0,0 +1,16 @@ + + + + + + + + anthem:command + + + + + + 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 index 000000000..ea185c5a5 --- /dev/null +++ b/bundles/org.openhab.binding.anthem/src/test/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParserTest.java @@ -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()); + } + } +}