[anthem] Add channel, refactor parser, add tests (#14720)

* Add channel and refactor parser

Signed-off-by: Mark Hilbush <mark@hilbush.com>
This commit is contained in:
Mark Hilbush 2023-05-04 17:26:14 -04:00 committed by GitHub
parent 891da2c944
commit 6ebfd84bdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 567 additions and 137 deletions

View 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" }

View 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";
}

View 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;
}

View 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;
}
}

View 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());
}
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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

View 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>
@ -50,6 +51,14 @@
</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>
@ -93,4 +102,11 @@
<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>

View File

@ -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>

View File

@ -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());
}
}
}