diff --git a/CODEOWNERS b/CODEOWNERS
index 3dd8c6bc328f3..6d685d130f306 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index c7a56633a3bb7..d28dbc58a7119 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -116,6 +116,11 @@
org.openhab.binding.anel
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.anthem
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.astro
diff --git a/bundles/org.openhab.binding.anthem/NOTICE b/bundles/org.openhab.binding.anthem/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/NOTICE
@@ -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
diff --git a/bundles/org.openhab.binding.anthem/README.md b/bundles/org.openhab.binding.anthem/README.md
new file mode 100644
index 0000000000000..0e84fab9d2b82
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/README.md
@@ -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" }
+```
diff --git a/bundles/org.openhab.binding.anthem/pom.xml b/bundles/org.openhab.binding.anthem/pom.xml
new file mode 100644
index 0000000000000..58014ae40a64e
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.0.0-SNAPSHOT
+
+
+ org.openhab.binding.anthem
+
+ openHAB Add-ons :: Bundles :: Anthem Binding
+
+
diff --git a/bundles/org.openhab.binding.anthem/src/main/feature/feature.xml b/bundles/org.openhab.binding.anthem/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..4dda4ccbc0855
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.anthem/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemBindingConstants.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemBindingConstants.java
new file mode 100644
index 0000000000000..a020f485f70c7
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemBindingConstants.java
@@ -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 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 = ';';
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemConfiguration.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemConfiguration.java
new file mode 100644
index 0000000000000..232ef9f2a6233
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemConfiguration.java
@@ -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 + " }";
+ }
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemHandlerFactory.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemHandlerFactory.java
new file mode 100644
index 0000000000000..dfffd7739648a
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/AnthemHandlerFactory.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommand.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommand.java
new file mode 100644
index 0000000000000..ed1797912115d
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommand.java
@@ -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();
+ }
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParser.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParser.java
new file mode 100644
index 0000000000000..8349093013b22
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemCommandParser.java
@@ -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 inputShortNamesMap = new HashMap<>();
+ private Map inputLongNamesMap = new HashMap<>();
+
+ private int numAvailableInputs;
+
+ public AnthemCommandParser(AnthemHandler anthemHandler) {
+ this.handler = anthemHandler;
+ }
+
+ public int getNumAvailableInputs() {
+ return numAvailableInputs;
+ }
+
+ public void parseMessage(String command) {
+ 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 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);
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java
new file mode 100644
index 0000000000000..3f66d753e335c
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/AnthemHandler.java
@@ -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 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");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/Zone.java b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/Zone.java
new file mode 100644
index 0000000000000..ba5ec097d2515
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/java/org/openhab/binding/anthem/internal/handler/Zone.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..642c0a14245ae
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,11 @@
+
+
+
+ binding
+ Anthem Binding
+ This is the binding for Anthem AV preamp/processors
+ local
+
+
diff --git a/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/i18n/anthem.properties b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/i18n/anthem.properties
new file mode 100644
index 0000000000000..fb89150d1939a
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/i18n/anthem.properties
@@ -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
diff --git a/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..33985294ba650
--- /dev/null
+++ b/bundles/org.openhab.binding.anthem/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+ Thing for Anthem AV processor
+
+
+
+
+ Controls zone 1 (the main zone) of the processor
+
+
+
+
+ Controls zone 2 of the processor
+
+
+
+
+
+
+ Host name or IP address of the Anthem AV processor
+ network-address
+
+
+
+
+ Network port number of the Anthem AV processor
+ 14999
+ true
+
+
+
+
+ The time to wait between reconnection attempts (in minutes)
+ 2
+ true
+
+
+
+
+ The delay between commands sent to the processor (in milliseconds)
+ 100
+ true
+
+
+
+
+
+
+ Channels for a zone of this processor
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ Set the volume level dB between -90 and 0
+ SoundVolume
+
+
+
+
+ Number
+
+ Selects the active input source
+
+
+
+ String
+
+ Short friendly name of the active input source
+
+
+
+
+ String
+
+ Long friendly name of the active input source
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index ed083d8c06907..65bb6d8345f31 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -58,6 +58,7 @@
org.openhab.binding.amplipi
org.openhab.binding.androiddebugbridge
org.openhab.binding.anel
+ org.openhab.binding.anthem
org.openhab.binding.astro
org.openhab.binding.atlona
org.openhab.binding.autelis