| 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
### 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" }
// 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";
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;
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";
}
return new AnthemCommand("IDN?");
}
+ public static AnthemCommand customCommand(String customCommand) {
+ return new AnthemCommand(customCommand);
+ }
+
public String getCommand() {
return command + COMMAND_TERMINATOR;
}
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;
*/
@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])");
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")) {
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) {
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
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) {
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) {
}
}
- 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;
}
}
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;
private @Nullable BufferedWriter writer;
private @Nullable BufferedReader reader;
- private AnthemCommandParser messageParser;
+ private AnthemCommandParser commandParser;
private final BlockingQueue<AnthemCommand> sendQueue = new LinkedBlockingQueue<>();
public AnthemHandler(Thing thing) {
super(thing);
- messageParser = new AnthemCommandParser(this);
+ commandParser = new AnthemCommandParser();
}
@Override
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()) {
}
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());
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);
}
}
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());
+ }
+ }
}
--- /dev/null
+/**
+ * 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");
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
# 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
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
<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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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());
+ }
+ }
+}