mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[anthem] Initial contribution of binding for Anthem AV preamp/processors (#14311)
* Initial contribution Signed-off-by: Mark Hilbush <mark@hilbush.com>
This commit is contained in:
parent
f98f820325
commit
749cf585ff
@ -25,6 +25,7 @@
|
||||
/bundles/org.openhab.binding.amplipi/ @kaikreuzer
|
||||
/bundles/org.openhab.binding.androiddebugbridge/ @GiviMAD
|
||||
/bundles/org.openhab.binding.anel/ @paphko
|
||||
/bundles/org.openhab.binding.anthem/ @mhilbush
|
||||
/bundles/org.openhab.binding.astro/ @gerrieg
|
||||
/bundles/org.openhab.binding.atlona/ @tmrobert8 @mlobstein
|
||||
/bundles/org.openhab.binding.autelis/ @digitaldan
|
||||
|
@ -116,6 +116,11 @@
|
||||
<artifactId>org.openhab.binding.anel</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.anthem</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.astro</artifactId>
|
||||
|
13
bundles/org.openhab.binding.anthem/NOTICE
Normal file
13
bundles/org.openhab.binding.anthem/NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
77
bundles/org.openhab.binding.anthem/README.md
Normal file
77
bundles/org.openhab.binding.anthem/README.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Anthem Binding
|
||||
|
||||
The binding allows control of Anthem AV processors over an IP connection to the processor.
|
||||
|
||||
## Supported Things
|
||||
|
||||
The following thing type is supported:
|
||||
|
||||
| Thing | ID | Discovery | Description |
|
||||
|----------|----------|-----------|-------------|
|
||||
| Anthem | anthem | Manual | Represents a Anthem AV processor |
|
||||
|
||||
Tested models include the AVM-60 11.2-channel preamp/processor.
|
||||
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
The following configuration parameters are available on the Anthem thing:
|
||||
|
||||
| Parameter | Parameter ID | Required/Optional | Description |
|
||||
|---------------------|---------------------------|-------------------|-------------|
|
||||
| Host | host | Required | IP address or host name of the Anthem AV processor |
|
||||
| Port | port | Optional | Port number used by the Anthem |
|
||||
| Reconnect Interval | reconnectIntervalMinutes | Optional | The time to wait between reconnection attempts (in minutes) |
|
||||
| Command Delay | commandDelayMsec | Optional | The delay between commands sent to the processor (in milliseconds) |
|
||||
|
||||
## Channels
|
||||
|
||||
The Anthem AV processor supports the following channels (some zones/channels are model specific):
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 2#activeInputShortName | String | Short friendly name of the active input |
|
||||
| 2#activeInputLongName | String | Long friendly name of the active input |
|
||||
|
||||
|
||||
## Full Example
|
||||
|
||||
### Things
|
||||
|
||||
```
|
||||
Thing anthem:anthem:mediaroom "Anthem AVM 60" [ host="192.168.1.100" ]
|
||||
```
|
||||
|
||||
### Items
|
||||
|
||||
```
|
||||
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" }
|
||||
Switch Anthem_Z1_Mute "Zone 1 Mute [%s]" { channel="anthem:anthem:mediaroom:1#mute" }
|
||||
Number Anthem_Z1_ActiveInput "Zone 1 Active Input [%.0f]" { channel="anthem:anthem:mediaroom:1#activeInput" }
|
||||
String Anthem_Z1_ActiveInputShortName "Zone 1 Active Input Short Name [%s]" { channel="anthem:anthem:mediaroom:1#activeInputShortName" }
|
||||
String Anthem_Z1_ActiveInputLongName "Zone 1 Active Input Long Name [%s]" { channel="anthem:anthem:mediaroom:1#activeInputLongName" }
|
||||
|
||||
Switch Anthem_Z2_Power "Zone 2 Power [%s]" { channel="anthem:anthem:mediaroom:1#power" }
|
||||
Dimmer Anthem_Z2_Volume "Zone 2 Volume [%s]" { channel="anthem:anthem:mediaroom:1#volume" }
|
||||
Number Anthem_Z2_Volume_DB "Zone 2 Volume dB [%.0f]" { channel="anthem:anthem:mediaroom:1#volumeDB" }
|
||||
Switch Anthem_Z2_Mute "Zone 2 Mute [%s]" { channel="anthem:anthem:mediaroom:1#mute" }
|
||||
Number Anthem_Z2_ActiveInput "Zone 2 Active Input [%.0f]" { channel="anthem:anthem:mediaroom:1#activeInput" }
|
||||
String Anthem_Z2_ActiveInputShortName "Zone 2 Active Input Short Name [%s]" { channel="anthem:anthem:mediaroom:1#activeInputShortName" }
|
||||
String Anthem_Z2_ActiveInputLongName "Zone 2 Active Input Long Name [%s]" { channel="anthem:anthem:mediaroom:1#activeInputLongName" }
|
||||
```
|
17
bundles/org.openhab.binding.anthem/pom.xml
Normal file
17
bundles/org.openhab.binding.anthem/pom.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>4.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.anthem</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Anthem Binding</name>
|
||||
|
||||
</project>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.anthem-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
|
||||
</repository>
|
||||
|
||||
<feature name="openhab-binding-anthem" description="Anthem Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.anthem/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link AnthemBindingConstants} class defines common constants, which are
|
||||
* used across the entire binding.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AnthemBindingConstants {
|
||||
public static final String BINDING_ID = "anthem";
|
||||
|
||||
public static final ThingTypeUID THING_TYPE_ANTHEM = new ThingTypeUID(BINDING_ID, "anthem");
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ANTHEM);
|
||||
|
||||
// Channel Ids
|
||||
public static final String CHANNEL_POWER = "power";
|
||||
public static final String CHANNEL_VOLUME = "volume";
|
||||
public static final String CHANNEL_VOLUME_DB = "volumeDB";
|
||||
public static final String CHANNEL_MUTE = "mute";
|
||||
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";
|
||||
|
||||
// Connection-related configuration parameters
|
||||
public static final int DEFAULT_PORT = 14999;
|
||||
public static final int DEFAULT_RECONNECT_INTERVAL_MINUTES = 2;
|
||||
public static final int DEFAULT_COMMAND_DELAY_MSEC = 100;
|
||||
|
||||
public static final char COMMAND_TERMINATION_CHAR = ';';
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import static org.openhab.binding.anthem.internal.AnthemBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AnthemConfiguration} is responsible for storing the Anthem thing configuration.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AnthemConfiguration {
|
||||
public String host = "";
|
||||
|
||||
public int port = DEFAULT_PORT;
|
||||
|
||||
public int reconnectIntervalMinutes = DEFAULT_RECONNECT_INTERVAL_MINUTES;
|
||||
|
||||
public int commandDelayMsec = DEFAULT_COMMAND_DELAY_MSEC;
|
||||
|
||||
public boolean isValid() {
|
||||
return !host.isBlank();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AnthemConfiguration{ host=" + host + ", port=" + port + ", reconectIntervalMinutes="
|
||||
+ reconnectIntervalMinutes + ", commandDelayMsec=" + commandDelayMsec + " }";
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import static org.openhab.binding.anthem.internal.AnthemBindingConstants.SUPPORTED_THING_TYPES_UIDS;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.anthem.internal.handler.AnthemHandler;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ConfigurationPolicy;
|
||||
|
||||
/**
|
||||
* The {@link AnthemHandlerFactory} is responsible for creating Anthem thing handlers.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.anthem", configurationPolicy = ConfigurationPolicy.OPTIONAL)
|
||||
public class AnthemHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
|
||||
return new AnthemHandler(thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 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.openhab.binding.anthem.internal.AnthemBindingConstants.COMMAND_TERMINATION_CHAR;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AnthemCommend} is responsible for creating commands to be sent to the
|
||||
* Anthem processor.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AnthemCommand {
|
||||
private static final String COMMAND_TERMINATOR = String.valueOf(COMMAND_TERMINATION_CHAR);
|
||||
|
||||
private String command = "";
|
||||
|
||||
public AnthemCommand(String command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
public static AnthemCommand powerOn(Zone zone) {
|
||||
return new AnthemCommand(String.format("Z%sPOW1", zone.getValue()));
|
||||
}
|
||||
|
||||
public static AnthemCommand powerOff(Zone zone) {
|
||||
return new AnthemCommand(String.format("Z%sPOW0", zone.getValue()));
|
||||
}
|
||||
|
||||
public static AnthemCommand volumeUp(Zone zone, int amount) {
|
||||
return new AnthemCommand(String.format("Z%sVUP%02d", zone.getValue(), amount));
|
||||
}
|
||||
|
||||
public static AnthemCommand volumeDown(Zone zone, int amount) {
|
||||
return new AnthemCommand(String.format("Z%sVDN%02d", zone.getValue(), amount));
|
||||
}
|
||||
|
||||
public static AnthemCommand volume(Zone zone, int level) {
|
||||
return new AnthemCommand(String.format("Z%sVOL%02d", zone.getValue(), level));
|
||||
}
|
||||
|
||||
public static AnthemCommand muteOn(Zone zone) {
|
||||
return new AnthemCommand(String.format("Z%sMUT1", zone.getValue()));
|
||||
}
|
||||
|
||||
public static AnthemCommand muteOff(Zone zone) {
|
||||
return new AnthemCommand(String.format("Z%sMUT0", zone.getValue()));
|
||||
}
|
||||
|
||||
public static AnthemCommand activeInput(Zone zone, int input) {
|
||||
return new AnthemCommand(String.format("Z%sINP%02d", zone.getValue(), input));
|
||||
}
|
||||
|
||||
public static AnthemCommand queryPower(Zone zone) {
|
||||
return new AnthemCommand(String.format("Z%sPOW?", zone.getValue()));
|
||||
}
|
||||
|
||||
public static AnthemCommand queryVolume(Zone zone) {
|
||||
return new AnthemCommand(String.format("Z%sVOL?", zone.getValue()));
|
||||
}
|
||||
|
||||
public static AnthemCommand queryMute(Zone zone) {
|
||||
return new AnthemCommand(String.format("Z%sMUT?", zone.getValue()));
|
||||
}
|
||||
|
||||
public static AnthemCommand queryActiveInput(Zone zone) {
|
||||
return new AnthemCommand(String.format("Z%sINP?", zone.getValue()));
|
||||
}
|
||||
|
||||
public static AnthemCommand queryNumAvailableInputs() {
|
||||
return new AnthemCommand(String.format("ICN?"));
|
||||
}
|
||||
|
||||
public static AnthemCommand queryInputShortName(int input) {
|
||||
return new AnthemCommand(String.format("ISN%02d?", input));
|
||||
}
|
||||
|
||||
public static AnthemCommand queryInputLongName(int input) {
|
||||
return new AnthemCommand(String.format("ILN%02d?", input));
|
||||
}
|
||||
|
||||
public static AnthemCommand queryModel() {
|
||||
return new AnthemCommand("IDM?");
|
||||
}
|
||||
|
||||
public static AnthemCommand queryRegion() {
|
||||
return new AnthemCommand("IDR?");
|
||||
}
|
||||
|
||||
public static AnthemCommand querySoftwareVersion() {
|
||||
return new AnthemCommand("IDS?");
|
||||
}
|
||||
|
||||
public static AnthemCommand querySoftwareBuildDate() {
|
||||
return new AnthemCommand("IDB?");
|
||||
}
|
||||
|
||||
public static AnthemCommand queryHardwareVersion() {
|
||||
return new AnthemCommand("IDH?");
|
||||
}
|
||||
|
||||
public static AnthemCommand queryMacAddress() {
|
||||
return new AnthemCommand("IDN?");
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return command + COMMAND_TERMINATOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getCommand();
|
||||
}
|
||||
}
|
@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 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.openhab.binding.anthem.internal.AnthemBindingConstants.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AnthemCommandParser} is responsible for parsing and handling
|
||||
* commands received from the Anthem processor.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AnthemCommandParser {
|
||||
private static final Pattern NUM_AVAILABLE_INPUTS_PATTERN = Pattern.compile("ICN([0-9])");
|
||||
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 static final Pattern VOLUME_PATTERN = Pattern.compile("Z([0-9])VOL(-?[0-9]*)");
|
||||
private static final Pattern MUTE_PATTERN = Pattern.compile("Z([0-9])MUT([01])");
|
||||
private static final Pattern ACTIVE_INPUT_PATTERN = Pattern.compile("Z([0-9])INP([1-9])");
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(AnthemCommandParser.class);
|
||||
|
||||
private AnthemHandler handler;
|
||||
|
||||
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) {
|
||||
if (!isValidCommand(command)) {
|
||||
return;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// Information command
|
||||
else if (cmd.startsWith("ID")) {
|
||||
parseInformationCommand(cmd);
|
||||
}
|
||||
// Number of inputs
|
||||
else if (cmd.startsWith("ICN")) {
|
||||
parseNumberOfAvailableInputsCommand(cmd);
|
||||
}
|
||||
// Input short name
|
||||
else if (cmd.startsWith("ISN")) {
|
||||
parseInputShortNameCommand(cmd);
|
||||
}
|
||||
// Input long name
|
||||
else if (cmd.startsWith("ILN")) {
|
||||
parseInputLongNameCommand(cmd);
|
||||
}
|
||||
// Error response to command
|
||||
else if (cmd.startsWith("!")) {
|
||||
parseErrorCommand(cmd);
|
||||
}
|
||||
// Unknown/unhandled command
|
||||
else {
|
||||
logger.trace("Command parser doesn't know how to handle command: '{}'", cmd);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidCommand(String command) {
|
||||
if (command.isEmpty() || command.isBlank() || command.length() < 4
|
||||
|| command.indexOf(COMMAND_TERMINATION_CHAR) == -1) {
|
||||
logger.trace("Parser received invalid command: '{}'", command);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void parseZoneCommand(String command) {
|
||||
// Power update
|
||||
if (command.contains("POW")) {
|
||||
parsePower(command);
|
||||
}
|
||||
// Volume update
|
||||
else if (command.contains("VOL")) {
|
||||
parseVolume(command);
|
||||
}
|
||||
// Mute update
|
||||
else if (command.contains("MUT")) {
|
||||
parseMute(command);
|
||||
}
|
||||
// Active input
|
||||
else if (command.contains("INP")) {
|
||||
parseActiveInput(command);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseInformationCommand(String command) {
|
||||
String value = command.substring(3, command.length());
|
||||
switch (command.substring(2, 3)) {
|
||||
case "M":
|
||||
handler.setModel(value);
|
||||
break;
|
||||
case "R":
|
||||
handler.setRegion(value);
|
||||
break;
|
||||
case "S":
|
||||
handler.setSoftwareVersion(value);
|
||||
break;
|
||||
case "B":
|
||||
handler.setSoftwareBuildDate(value);
|
||||
break;
|
||||
case "H":
|
||||
handler.setHardwareVersion(value);
|
||||
break;
|
||||
case "N":
|
||||
handler.setMacAddress(value);
|
||||
break;
|
||||
case "Q":
|
||||
// Ignore
|
||||
break;
|
||||
default:
|
||||
logger.debug("Unknown info type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void 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) {
|
||||
logger.debug("Parsing exception on command: {}", command, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseInputShortNameCommand(String command) {
|
||||
parseInputName(command, INPUT_SHORT_NAME_PATTERN.matcher(command), inputShortNamesMap);
|
||||
}
|
||||
|
||||
private void parseInputLongNameCommand(String command) {
|
||||
parseInputName(command, INPUT_LONG_NAME_PATTERN.matcher(command), inputLongNamesMap);
|
||||
}
|
||||
|
||||
private void parseErrorCommand(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) {
|
||||
if (matcher != null) {
|
||||
try {
|
||||
matcher.find();
|
||||
int input = Integer.parseInt(matcher.group(1));
|
||||
String inputName = matcher.group(2);
|
||||
map.putIfAbsent(input, inputName);
|
||||
} catch (NumberFormatException | IndexOutOfBoundsException | IllegalStateException e) {
|
||||
logger.debug("Parsing exception on command: {}", command, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void 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);
|
||||
} catch (IndexOutOfBoundsException | IllegalStateException e) {
|
||||
logger.debug("Parsing exception on command: {}", command, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void 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));
|
||||
} catch (NumberFormatException | IndexOutOfBoundsException | IllegalStateException e) {
|
||||
logger.debug("Parsing exception on command: {}", command, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void 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);
|
||||
} catch (IndexOutOfBoundsException | IllegalStateException e) {
|
||||
logger.debug("Parsing exception on command: {}", command, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void 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));
|
||||
}
|
||||
} catch (NumberFormatException | IndexOutOfBoundsException | IllegalStateException e) {
|
||||
logger.debug("Parsing exception on command: {}", command, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,437 @@
|
||||
/**
|
||||
* 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.openhab.binding.anthem.internal.AnthemBindingConstants.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AnthemHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels. It also manages the connection to the AV processor.
|
||||
* The reader thread receives solicited and unsolicited commands from the processor.
|
||||
* The sender thread is used to send commands to the processor.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AnthemHandler extends BaseThingHandler {
|
||||
private Logger logger = LoggerFactory.getLogger(AnthemHandler.class);
|
||||
|
||||
private static final long POLLING_INTERVAL_SECONDS = 900L;
|
||||
private static final long POLLING_DELAY_SECONDS = 10L;
|
||||
|
||||
private @Nullable Socket socket;
|
||||
private @Nullable BufferedWriter writer;
|
||||
private @Nullable BufferedReader reader;
|
||||
|
||||
private AnthemCommandParser messageParser;
|
||||
|
||||
private final BlockingQueue<AnthemCommand> sendQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
private @Nullable Future<?> asyncInitializeTask;
|
||||
private @Nullable ScheduledFuture<?> connectRetryJob;
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
|
||||
private @Nullable Thread senderThread;
|
||||
private @Nullable Thread readerThread;
|
||||
|
||||
private int reconnectIntervalMinutes;
|
||||
private int commandDelayMsec;
|
||||
|
||||
private boolean zone1PreviousPowerState;
|
||||
private boolean zone2PreviousPowerState;
|
||||
|
||||
public AnthemHandler(Thing thing) {
|
||||
super(thing);
|
||||
messageParser = new AnthemCommandParser(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
AnthemConfiguration configuration = getConfig().as(AnthemConfiguration.class);
|
||||
logger.debug("AnthemHandler: Configuration of thing {} is {}", thing.getUID().getId(), configuration);
|
||||
|
||||
if (!configuration.isValid()) {
|
||||
logger.debug("AnthemHandler: Config of thing '{}' is invalid", thing.getUID().getId());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/thing-status-detail-invalidconfig");
|
||||
return;
|
||||
}
|
||||
reconnectIntervalMinutes = configuration.reconnectIntervalMinutes;
|
||||
commandDelayMsec = configuration.commandDelayMsec;
|
||||
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/thing-status-detail-connecting");
|
||||
asyncInitializeTask = scheduler.submit(this::connect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
Future<?> localAsyncInitializeTask = this.asyncInitializeTask;
|
||||
if (localAsyncInitializeTask != null) {
|
||||
localAsyncInitializeTask.cancel(true);
|
||||
this.asyncInitializeTask = null;
|
||||
}
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command {} received for channel {}", command, channelUID.getId().toString());
|
||||
String groupId = channelUID.getGroupId();
|
||||
if (groupId == null) {
|
||||
return;
|
||||
}
|
||||
Zone zone = Zone.fromValue(groupId);
|
||||
|
||||
switch (channelUID.getIdWithoutGroup()) {
|
||||
case CHANNEL_POWER:
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
// Power on the device
|
||||
sendCommand(AnthemCommand.powerOn(zone));
|
||||
} else if (command == OnOffType.OFF) {
|
||||
sendCommand(AnthemCommand.powerOff(zone));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_VOLUME:
|
||||
if (command instanceof OnOffType || command instanceof IncreaseDecreaseType) {
|
||||
if (command == OnOffType.ON || command == IncreaseDecreaseType.INCREASE) {
|
||||
sendCommand(AnthemCommand.volumeUp(zone, 1));
|
||||
} else if (command == OnOffType.OFF || command == IncreaseDecreaseType.DECREASE) {
|
||||
sendCommand(AnthemCommand.volumeDown(zone, 1));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_VOLUME_DB:
|
||||
if (command instanceof DecimalType) {
|
||||
sendCommand(AnthemCommand.volume(zone, ((DecimalType) command).intValue()));
|
||||
}
|
||||
break;
|
||||
case CHANNEL_MUTE:
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
sendCommand(AnthemCommand.muteOn(zone));
|
||||
} else if (command == OnOffType.OFF) {
|
||||
sendCommand(AnthemCommand.muteOff(zone));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_ACTIVE_INPUT:
|
||||
if (command instanceof DecimalType) {
|
||||
sendCommand(AnthemCommand.activeInput(zone, ((DecimalType) command).intValue()));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("Received 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());
|
||||
sendCommand(AnthemCommand.queryModel());
|
||||
sendCommand(AnthemCommand.queryRegion());
|
||||
sendCommand(AnthemCommand.querySoftwareVersion());
|
||||
sendCommand(AnthemCommand.querySoftwareBuildDate());
|
||||
sendCommand(AnthemCommand.queryHardwareVersion());
|
||||
sendCommand(AnthemCommand.queryMacAddress());
|
||||
sendCommand(AnthemCommand.queryVolume(zone));
|
||||
sendCommand(AnthemCommand.queryMute(zone));
|
||||
// Give some time for the input names to populate before requesting the active input
|
||||
scheduler.schedule(() -> queryActiveInput(zone), 5L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void queryActiveInput(Zone zone) {
|
||||
sendCommand(AnthemCommand.queryActiveInput(zone));
|
||||
}
|
||||
|
||||
private void sendCommand(AnthemCommand command) {
|
||||
logger.debug("Adding command to queue: {}", command);
|
||||
sendQueue.add(command);
|
||||
}
|
||||
|
||||
private synchronized void connect() {
|
||||
try {
|
||||
AnthemConfiguration configuration = getConfig().as(AnthemConfiguration.class);
|
||||
logger.debug("Opening connection to Anthem host {} on port {}", configuration.host, configuration.port);
|
||||
Socket socket = new Socket(configuration.host, configuration.port);
|
||||
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.ISO_8859_1));
|
||||
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.ISO_8859_1));
|
||||
this.socket = socket;
|
||||
} catch (UnknownHostException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/thing-status-detail-unknownhost");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/thing-status-detail-invalidport");
|
||||
return;
|
||||
} catch (InterruptedIOException e) {
|
||||
logger.debug("Interrupted while establishing Anthem connection");
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/thing-status-detail-openerror");
|
||||
logger.debug("Error opening Anthem connection: {}", e.getMessage());
|
||||
disconnect();
|
||||
scheduleConnectRetry(reconnectIntervalMinutes);
|
||||
return;
|
||||
}
|
||||
Thread localReaderThread = new Thread(this::readerThreadJob, "Anthem reader");
|
||||
localReaderThread.setDaemon(true);
|
||||
localReaderThread.start();
|
||||
this.readerThread = localReaderThread;
|
||||
|
||||
Thread localSenderThread = new Thread(this::senderThreadJob, "Anthem sender");
|
||||
localSenderThread.setDaemon(true);
|
||||
localSenderThread.start();
|
||||
this.senderThread = localSenderThread;
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
ScheduledFuture<?> localPollingJob = this.pollingJob;
|
||||
if (localPollingJob == null) {
|
||||
this.pollingJob = scheduler.scheduleWithFixedDelay(this::poll, POLLING_DELAY_SECONDS,
|
||||
POLLING_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void poll() {
|
||||
logger.debug("Polling...");
|
||||
sendCommand(AnthemCommand.queryPower(Zone.MAIN));
|
||||
sendCommand(AnthemCommand.queryPower(Zone.ZONE2));
|
||||
}
|
||||
|
||||
private void scheduleConnectRetry(long waitMinutes) {
|
||||
logger.debug("Scheduling connection retry in {} minutes", waitMinutes);
|
||||
connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private synchronized void disconnect() {
|
||||
logger.debug("Disconnecting from Anthem");
|
||||
|
||||
ScheduledFuture<?> localPollingJob = this.pollingJob;
|
||||
if (localPollingJob != null) {
|
||||
localPollingJob.cancel(true);
|
||||
this.pollingJob = null;
|
||||
}
|
||||
|
||||
ScheduledFuture<?> localConnectRetryJob = this.connectRetryJob;
|
||||
if (localConnectRetryJob != null) {
|
||||
localConnectRetryJob.cancel(true);
|
||||
this.connectRetryJob = null;
|
||||
}
|
||||
|
||||
Thread localSenderThread = this.senderThread;
|
||||
if (localSenderThread != null && localSenderThread.isAlive()) {
|
||||
localSenderThread.interrupt();
|
||||
}
|
||||
|
||||
Thread localReaderThread = this.readerThread;
|
||||
if (localReaderThread != null && localReaderThread.isAlive()) {
|
||||
localReaderThread.interrupt();
|
||||
}
|
||||
Socket localSocket = this.socket;
|
||||
if (localSocket != null) {
|
||||
try {
|
||||
localSocket.close();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Error closing socket: {}", e.getMessage());
|
||||
}
|
||||
this.socket = null;
|
||||
}
|
||||
BufferedReader localReader = this.reader;
|
||||
if (localReader != null) {
|
||||
try {
|
||||
localReader.close();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Error closing reader: {}", e.getMessage());
|
||||
}
|
||||
this.reader = null;
|
||||
}
|
||||
BufferedWriter localWriter = this.writer;
|
||||
if (localWriter != null) {
|
||||
try {
|
||||
localWriter.close();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Error closing writer: {}", e.getMessage());
|
||||
}
|
||||
this.writer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void reconnect() {
|
||||
logger.debug("Attempting to reconnect to the Anthem");
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
|
||||
private void senderThreadJob() {
|
||||
logger.debug("Sender thread started");
|
||||
try {
|
||||
while (!Thread.currentThread().isInterrupted() && writer != null) {
|
||||
AnthemCommand command = sendQueue.take();
|
||||
logger.debug("Sender thread writing command: {}", command);
|
||||
try {
|
||||
BufferedWriter localWriter = this.writer;
|
||||
if (localWriter != null) {
|
||||
localWriter.write(command.toString());
|
||||
localWriter.flush();
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
logger.debug("Interrupted while sending command");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/thing-status-detail-interrupted");
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
logger.debug("Communication error, will try to reconnect. Error: {}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
// Requeue the command and try to reconnect
|
||||
sendQueue.add(command);
|
||||
reconnect();
|
||||
break;
|
||||
}
|
||||
// Introduce delay to throttle the send rate
|
||||
if (commandDelayMsec > 0) {
|
||||
Thread.sleep(commandDelayMsec);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
logger.debug("Sender thread exiting");
|
||||
}
|
||||
}
|
||||
|
||||
private void readerThreadJob() {
|
||||
logger.debug("Reader thread started");
|
||||
StringBuffer sbReader = new StringBuffer();
|
||||
try {
|
||||
char c;
|
||||
String command;
|
||||
BufferedReader localReader = this.reader;
|
||||
while (!Thread.interrupted() && localReader != null) {
|
||||
c = (char) localReader.read();
|
||||
sbReader.append(c);
|
||||
if (c == COMMAND_TERMINATION_CHAR) {
|
||||
command = sbReader.toString();
|
||||
logger.debug("Reader thread sending command to parser: {}", command);
|
||||
messageParser.parseMessage(command);
|
||||
sbReader.setLength(0);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
logger.debug("Interrupted while reading");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/thing-status-detail-interrupted");
|
||||
} catch (IOException e) {
|
||||
logger.debug("I/O error while reading from socket: {}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/thing-status-detail-ioexception");
|
||||
} finally {
|
||||
logger.debug("Reader thread exiting");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 Zone} defines the zones supported by the Anthem processor.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Zone {
|
||||
MAIN("1"),
|
||||
ZONE2("2");
|
||||
|
||||
private final String value;
|
||||
|
||||
Zone(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public static Zone fromValue(String value) {
|
||||
for (Zone m : Zone.values()) {
|
||||
if (m.getValue().equals(value)) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid or null zone: " + value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="anthem" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>Anthem Binding</name>
|
||||
<description>This is the binding for Anthem AV preamp/processors</description>
|
||||
<connection>local</connection>
|
||||
|
||||
</addon:addon>
|
@ -0,0 +1,50 @@
|
||||
# add-on
|
||||
|
||||
addon.anthem.name = Anthem Binding
|
||||
addon.anthem.description = This is the binding for Anthem AV preamp/processors
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.anthem.anthem.label = Anthem
|
||||
thing-type.anthem.anthem.description = Thing for Anthem AV processor
|
||||
thing-type.anthem.anthem.group.1.label = Main Zone
|
||||
thing-type.anthem.anthem.group.1.description = Controls zone 1 (the main zone) of the processor
|
||||
thing-type.anthem.anthem.group.2.label = Zone 2
|
||||
thing-type.anthem.anthem.group.2.description = Controls zone 2 of the processor
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.anthem.anthem.commandDelayMsec.label = Command Delay
|
||||
thing-type.config.anthem.anthem.commandDelayMsec.description = The delay between commands sent to the processor (in milliseconds)
|
||||
thing-type.config.anthem.anthem.host.label = Network Address
|
||||
thing-type.config.anthem.anthem.host.description = Host name or IP address of the Anthem AV processor
|
||||
thing-type.config.anthem.anthem.port.label = Network Port
|
||||
thing-type.config.anthem.anthem.port.description = Network port number of the Anthem AV processor
|
||||
thing-type.config.anthem.anthem.reconnectIntervalMinutes.label = Reconnect Interval
|
||||
thing-type.config.anthem.anthem.reconnectIntervalMinutes.description = The time to wait between reconnection attempts (in minutes)
|
||||
|
||||
# channel group types
|
||||
|
||||
channel-group-type.anthem.zone.label = Zone Control
|
||||
channel-group-type.anthem.zone.description = Channels for a zone of this processor
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.anthem.activeInput.label = Active Input
|
||||
channel-type.anthem.activeInput.description = Selects the active input source
|
||||
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.volumeDB.label = Volume dB
|
||||
channel-type.anthem.volumeDB.description = Set the volume level dB between -90 and 0
|
||||
|
||||
# thing status detail messages
|
||||
|
||||
thing-status-detail-connecting = Connecting
|
||||
thing-status-detail-unknownhost = Unknown host
|
||||
thing-status-detail-invalidport = Invalid port number
|
||||
thing-status-detail-openerror = Error opening Anthem connection. Check log
|
||||
thing-status-detail-interrupted = Interrupted
|
||||
thing-status-detail-ioerror = I/O Error
|
||||
thing-status-detail-invalidconfig = Invalid Anthem thing configuration
|
@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="anthem"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="anthem">
|
||||
<label>Anthem</label>
|
||||
<description>Thing for Anthem AV processor</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="1" typeId="zone">
|
||||
<label>Main Zone</label>
|
||||
<description>Controls zone 1 (the main zone) of the processor</description>
|
||||
</channel-group>
|
||||
|
||||
<channel-group id="2" typeId="zone">
|
||||
<label>Zone 2</label>
|
||||
<description>Controls zone 2 of the processor</description>
|
||||
</channel-group>
|
||||
</channel-groups>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<label>Network Address</label>
|
||||
<description>Host name or IP address of the Anthem AV processor</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
|
||||
<parameter name="port" type="integer">
|
||||
<label>Network Port</label>
|
||||
<description>Network port number of the Anthem AV processor</description>
|
||||
<default>14999</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="reconnectIntervalMinutes" type="integer">
|
||||
<label>Reconnect Interval</label>
|
||||
<description>The time to wait between reconnection attempts (in minutes)</description>
|
||||
<default>2</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="commandDelayMsec" type="integer">
|
||||
<label>Command Delay</label>
|
||||
<description>The delay between commands sent to the processor (in milliseconds)</description>
|
||||
<default>100</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-group-type id="zone">
|
||||
<label>Zone Control</label>
|
||||
<description>Channels for a zone of this processor</description>
|
||||
<channels>
|
||||
<channel id="power" typeId="system.power"/>
|
||||
<channel id="volume" typeId="system.volume"/>
|
||||
<channel id="volumeDB" typeId="volumeDB"/>
|
||||
<channel id="mute" typeId="system.mute"/>
|
||||
<channel id="activeInput" typeId="activeInput"/>
|
||||
<channel id="activeInputShortName" typeId="activeInputShortName"/>
|
||||
<channel id="activeInputLongName" typeId="activeInputLongName"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<!-- Channel types -->
|
||||
<channel-type id="volumeDB" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Volume dB</label>
|
||||
<description>Set the volume level dB between -90 and 0</description>
|
||||
<category>SoundVolume</category>
|
||||
<state min="-90" max="0" step="1" pattern="%.0f dB"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="activeInput">
|
||||
<item-type>Number</item-type>
|
||||
<label>Active Input</label>
|
||||
<description>Selects the active input source</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="activeInputShortName">
|
||||
<item-type>String</item-type>
|
||||
<label>Active Input Short Name</label>
|
||||
<description>Short friendly name of the active input source</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="activeInputLongName" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Active Input Long Name</label>
|
||||
<description>Long friendly name of the active input source</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -58,6 +58,7 @@
|
||||
<module>org.openhab.binding.amplipi</module>
|
||||
<module>org.openhab.binding.androiddebugbridge</module>
|
||||
<module>org.openhab.binding.anel</module>
|
||||
<module>org.openhab.binding.anthem</module>
|
||||
<module>org.openhab.binding.astro</module>
|
||||
<module>org.openhab.binding.atlona</module>
|
||||
<module>org.openhab.binding.autelis</module>
|
||||
|
Loading…
Reference in New Issue
Block a user