[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.amplipi/ @kaikreuzer
|
||||||
/bundles/org.openhab.binding.androiddebugbridge/ @GiviMAD
|
/bundles/org.openhab.binding.androiddebugbridge/ @GiviMAD
|
||||||
/bundles/org.openhab.binding.anel/ @paphko
|
/bundles/org.openhab.binding.anel/ @paphko
|
||||||
|
/bundles/org.openhab.binding.anthem/ @mhilbush
|
||||||
/bundles/org.openhab.binding.astro/ @gerrieg
|
/bundles/org.openhab.binding.astro/ @gerrieg
|
||||||
/bundles/org.openhab.binding.atlona/ @tmrobert8 @mlobstein
|
/bundles/org.openhab.binding.atlona/ @tmrobert8 @mlobstein
|
||||||
/bundles/org.openhab.binding.autelis/ @digitaldan
|
/bundles/org.openhab.binding.autelis/ @digitaldan
|
||||||
|
|
|
@ -116,6 +116,11 @@
|
||||||
<artifactId>org.openhab.binding.anel</artifactId>
|
<artifactId>org.openhab.binding.anel</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.binding.anthem</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.astro</artifactId>
|
<artifactId>org.openhab.binding.astro</artifactId>
|
||||||
|
|
|
@ -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
|
|
@ -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" }
|
||||||
|
```
|
|
@ -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.amplipi</module>
|
||||||
<module>org.openhab.binding.androiddebugbridge</module>
|
<module>org.openhab.binding.androiddebugbridge</module>
|
||||||
<module>org.openhab.binding.anel</module>
|
<module>org.openhab.binding.anel</module>
|
||||||
|
<module>org.openhab.binding.anthem</module>
|
||||||
<module>org.openhab.binding.astro</module>
|
<module>org.openhab.binding.astro</module>
|
||||||
<module>org.openhab.binding.atlona</module>
|
<module>org.openhab.binding.atlona</module>
|
||||||
<module>org.openhab.binding.autelis</module>
|
<module>org.openhab.binding.autelis</module>
|
||||||
|
|
Loading…
Reference in New Issue