added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.monopriceaudio</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,149 @@
# Monoprice Whole House Audio Binding
This binding can be used to control the Monoprice MPR-SG6Z (10761) or Dayton Audio DAX66 whole house multi-zone amplifier.
All controller functions available through the serial port interface can be controlled by the binding.
Up to 18 zones can be controlled when 3 amplifiers are connected together (if not all zones on the amp are used they can be excluded via configuration).
Activating the 'Page All Zones' feature can only be done through the +12v trigger input on the back of the amplifier.
The binding supports two different kinds of connections:
* serial connection,
* serial over IP connection
For users without serial connector on the server side, you can add a serial to USB adapter.
You don't need to have your Monoprice whole house amplifier device directly connected to your openHAB server.
You can connect it for example to a Raspberry Pi and use [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) to make the serial connection available on LAN (serial over IP).
## Supported Things
There is exactly one supported thing type, which represents the amplifier controller.
It has the `amplifier` id.
## Discovery
Discovery is not supported.
You have to add all things manually.
## Binding Configuration
There are no overall binding configuration settings that need to be set.
All settings are through thing configuration parameters.
## Thing Configuration
The thing has the following configuration parameters:
| Parameter Label | Parameter ID | Description | Accepted values |
|----------------------|------------------|--------------------------------------------------------------------------------------------------------------------------------|------------------|
| Serial Port | serialPort | Serial port to use for connecting to the Monoprice whole house amplifier device | Serial port name |
| Address | host | Host name or IP address of the machine connected to the Monoprice whole house amplifier device (serial over IP) | Host name or IP |
| Port | port | Communication port (serial over IP). | TCP port number |
| Number of Zones | numZones | (Optional) Number of amplifier zones to utilize in the binding (up to 18 zones with 3 amplifiers connected together) | 1-18; default 6 |
| Polling Interval | pollingInterval | (Optional) Configures how often (in seconds) to poll the controller to check for zone updates | 5-60; default 15 |
| Ignore Zones | ignoreZones | (Optional) A comma seperated list of Zone numbers that will ignore the 'All Zone' (except All Off) commands | ie: "1,6,10" |
| Initial All Volume | initialAllVolume | (Optional) When 'All' zones are activated, the volume will reset to this value to prevent excessive blaring of sound ;) | 1-30; default 10 |
| Source 1 Input Label | inputLabel1 | (Optional) Friendly name for the input source to be displayed in the UI (ie: Chromecast, Radio, CD, etc.) (default "Source 1") | A free text name |
| Source 2 Input Label | inputLabel2 | (Optional) Friendly name for the input source to be displayed in the UI (ie: Chromecast, Radio, CD, etc.) (default "Source 2") | A free text name |
| Source 3 Input Label | inputLabel3 | (Optional) Friendly name for the input source to be displayed in the UI (ie: Chromecast, Radio, CD, etc.) (default "Source 3") | A free text name |
| Source 4 Input Label | inputLabel4 | (Optional) Friendly name for the input source to be displayed in the UI (ie: Chromecast, Radio, CD, etc.) (default "Source 4") | A free text name |
| Source 5 Input Label | inputLabel5 | (Optional) Friendly name for the input source to be displayed in the UI (ie: Chromecast, Radio, CD, etc.) (default "Source 5") | A free text name |
| Source 6 Input Label | inputLabel6 | (Optional) Friendly name for the input source to be displayed in the UI (ie: Chromecast, Radio, CD, etc.) (default "Source 6") | A free text name |
Some notes:
* On Linux, you may get an error stating the serial port cannot be opened when the MonopriceAudio binding tries to load.
* You can get around this by adding the `openhab` user to the `dialout` group like this: `usermod -a -G dialout openhab`.
* Also on Linux you may have issues with the USB if using two serial USB devices e.g. MonopriceAudio and RFXcom.
* See the [general documentation about serial port configuration](/docs/administration/serial.html) for more on symlinking the USB ports.
* Here is an example of ser2net.conf you can use to share your serial port /dev/ttyUSB0 on IP port 4444 using [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) (take care, the baud rate is specific to the Monoprice amplifier):
```
4444:raw:0:/dev/ttyUSB0:9600 8DATABITS NONE 1STOPBIT LOCAL
```
## Channels
The following channels are available:
| Channel ID | Item Type | Description |
|-------------------------------|-----------|---------------------------------------------------------------------------------------------------------------|
| all#allpower | Switch | Turn all zones on or off simultaneously (those specified by the ignoreZones config option will not turn on) |
| all#allsource | Number | Select the input source for all zones simultaneously (1-6) (except ignoreZones) |
| all#allvolume | Dimmer | Control the volume for all zones simultaneously (0-100%) [translates to 0-38] (except ignoreZones) |
| all#allmute | Switch | Mute or unmute all zones simultaneously (except ignoreZones) |
| zoneN#power (where N= 1-18) | Switch | Turn the power for a zone on or off |
| zoneN#source (where N= 1-18) | Number | Select the input source for a zone (1-6) |
| zoneN#volume (where N= 1-18) | Dimmer | Control the volume for a zone (0-100%) [translates to 0-38] |
| zoneN#mute (where N= 1-18) | Switch | Mute or unmute a zone |
| zoneN#treble (where N= 1-18) | Number | Adjust the treble control for a zone (-7 to 7) -7=none, 0=flat, 7=full |
| zoneN#bass (where N= 1-18) | Number | Adjust the bass control for a zone (-7 to 7) -7=none, 0=flat, 7=full |
| zoneN#balance (where N= 1-18) | Number | Adjust the balance control for a zone (-10 to 10) -10=left, 0=center, 10=right |
| zoneN#dnd (where N= 1-18) | Switch | Turn on or off the Do Not Disturb for the zone (for when the controller's external page trigger is activated) |
| zoneN#page (where N= 1-18) | Contact | Indicates if the page input is activated for the zone |
| zoneN#keypad (where N= 1-18) | Contact | Indicates if the physical keypad is attached to a zone |
## Full Example
monoprice.things:
```java
//serial port connection
monopriceaudio:amplifier:myamp "Monoprice WHA" [ serialPort="COM5", pollingInterval=15, numZones=6, inputLabel1="Chromecast", inputLabel2="Radio", inputLabel3="CD Player", inputLabel4="Bluetooth Audio", inputLabel5="HTPC", inputLabel6="Phono"]
// serial over IP connection
monopriceaudio:amplifier:myamp "Monoprice WHA" [ host="192.168.0.10", port=4444, pollingInterval=15, numZones=6, inputLabel1="Chromecast", inputLabel2="Radio", inputLabel3="CD Player", inputLabel4="Bluetooth Audio", inputLabel5="HTPC", inputLabel6="Phono"]
```
monoprice.items:
```java
Switch all_allpower "All Zones Power" { channel="monopriceaudio:amplifier:myamp:all#allpower" }
Number all_source "Source Input [%s]" { channel="monopriceaudio:amplifier:myamp:all#allsource" }
Dimmer all_volume "Volume [%d %%]" { channel="monopriceaudio:amplifier:myamp:all#allvolume" }
Switch all_mute "Mute" { channel="monopriceaudio:amplifier:myamp:all#allmute" }
Switch z1_power "Power" { channel="monopriceaudio:amplifier:myamp:zone1#power" }
Number z1_source "Source Input [%s]" { channel="monopriceaudio:amplifier:myamp:zone1#source" }
Dimmer z1_volume "Volume [%d %%]" { channel="monopriceaudio:amplifier:myamp:zone1#volume" }
Switch z1_mute "Mute" { channel="monopriceaudio:amplifier:myamp:zone1#mute" }
Number z1_treble "Treble Adjustment [%s]" { channel="monopriceaudio:amplifier:myamp:zone1#treble" }
Number z1_bass "Bass Adjustment [%s]" { channel="monopriceaudio:amplifier:myamp:zone1#bass" }
Number z1_balance "Balance Adjustment [%s]" { channel="monopriceaudio:amplifier:myamp:zone1#balance" }
Switch z1_dnd "Do Not Disturb" { channel="monopriceaudio:amplifier:myamp:zone1#dnd" }
Switch z1_page "Page Active: [%s]" { channel="monopriceaudio:amplifier:myamp:zone1#page" }
Switch z1_keypad "Keypad Connected: [%s]" { channel="monopriceaudio:amplifier:myamp:zone1#keypad" }
//repeat for zones 2-18 (substitute z1 and zone1)
```
monoprice.sitemap:
```perl
sitemap monoprice label="Audio Control" {
Frame label="All Zones" {
Switch item=all_allpower label="All Zones On" mappings=[ON=" "]
Switch item=all_allpower label="All Zones Off" mappings=[OFF=" "]
Selection item=all_source
Setpoint item=all_volume minValue=0 maxValue=100 step=1
Switch item=all_mute
}
Frame label="Zone 1" {
Switch item=z1_power
Selection item=z1_source visibility=[z1_power==ON]
//Volume can be a Slider also
Setpoint item=z1_volume minValue=0 maxValue=100 step=1 visibility=[z1_power==ON]
Switch item=z1_mute visibility=[z1_power==ON]
Setpoint item=z1_treble label="Treble Adjustment [%d]" minValue=-7 maxValue=7 step=1 visibility=[z1_power==ON]
Setpoint item=z1_bass label="Bass Adjustment [%d]" minValue=-7 maxValue=7 step=1 visibility=[z1_power==ON]
Setpoint item=z1_balance label="Balance Adjustment [%d]" minValue=-10 maxValue=10 step=1 visibility=[z1_power==ON]
Switch item=z1_dnd visibility=[z1_power==ON]
Text item=z1_page label="Page Active: [%s]" visibility=[z1_power==ON]
Text item=z1_keypad label="Keypad Connected: [%s]" visibility=[z1_power==ON]
}
//repeat for zones 2-18 (substitute z1)
}
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.monopriceaudio</artifactId>
<name>openHAB Add-ons :: Bundles :: MonopriceAudio Binding</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.monopriceaudio-${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-monopriceaudio" description="Monoprice Whole House Audio Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.monopriceaudio/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link MonopriceAudioBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class MonopriceAudioBindingConstants {
public static final String BINDING_ID = "monopriceaudio";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_AMP = new ThingTypeUID(BINDING_ID, "amplifier");
// List of all Channel types
public static final String CHANNEL_TYPE_POWER = "power";
public static final String CHANNEL_TYPE_SOURCE = "source";
public static final String CHANNEL_TYPE_VOLUME = "volume";
public static final String CHANNEL_TYPE_MUTE = "mute";
public static final String CHANNEL_TYPE_TREBLE = "treble";
public static final String CHANNEL_TYPE_BASS = "bass";
public static final String CHANNEL_TYPE_BALANCE = "balance";
public static final String CHANNEL_TYPE_DND = "dnd";
public static final String CHANNEL_TYPE_PAGE = "page";
public static final String CHANNEL_TYPE_KEYPAD = "keypad";
public static final String CHANNEL_TYPE_ALLPOWER = "allpower";
public static final String CHANNEL_TYPE_ALLSOURCE = "allsource";
public static final String CHANNEL_TYPE_ALLVOLUME = "allvolume";
public static final String CHANNEL_TYPE_ALLMUTE = "allmute";
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MonopriceAudioException} class is used for any exception thrown by the binding
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class MonopriceAudioException extends Exception {
private static final long serialVersionUID = 1L;
public MonopriceAudioException() {
}
public MonopriceAudioException(String message, Throwable t) {
super(message, t);
}
public MonopriceAudioException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal;
import static org.openhab.binding.monopriceaudio.internal.MonopriceAudioBindingConstants.*;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.monopriceaudio.internal.handler.MonopriceAudioHandler;
import org.openhab.core.io.transport.serial.SerialPortManager;
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.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link MonopriceAudioHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.monopriceaudio", service = ThingHandlerFactory.class)
public class MonopriceAudioHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AMP);
private final SerialPortManager serialPortManager;
private final MonopriceAudioStateDescriptionOptionProvider stateDescriptionProvider;
@Activate
public MonopriceAudioHandlerFactory(final @Reference MonopriceAudioStateDescriptionOptionProvider provider,
final @Reference SerialPortManager serialPortManager) {
this.stateDescriptionProvider = provider;
this.serialPortManager = serialPortManager;
}
@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 MonopriceAudioHandler(thing, stateDescriptionProvider, serialPortManager);
}
return null;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options while leaving other state description fields as original.
*
* @author Michael Lobstein - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, MonopriceAudioStateDescriptionOptionProvider.class })
@NonNullByDefault
public class MonopriceAudioStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents the different kinds of commands
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public enum MonopriceAudioCommand {
QUERY("?"),
POWER("PR"),
SOURCE("CH"),
VOLUME("VO"),
MUTE("MU"),
TREBLE("TR"),
BASS("BS"),
BALANCE("BL"),
DND("DT");
private final String value;
MonopriceAudioCommand(String value) {
this.value = value;
}
/**
* Get the command name
*
* @return the command name
*/
public String getValue() {
return value;
}
}

View File

@@ -0,0 +1,264 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
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.binding.monopriceaudio.internal.MonopriceAudioException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class for communicating with the MonopriceAudio device
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the MonopriceAudio binding
*/
@NonNullByDefault
public abstract class MonopriceAudioConnector {
public static final String READ_ERROR = "Command Error.";
// Message types
public static final String KEY_ZONE_UPDATE = "zone_update";
// Special keys used by the binding
public static final String KEY_ERROR = "error";
public static final String MSG_VALUE_ON = "on";
private static final Pattern PATTERN = Pattern.compile("^.*#>(\\d{22})$", Pattern.DOTALL);
private static final String BEGIN_CMD = "<";
private static final String END_CMD = "\r";
private final Logger logger = LoggerFactory.getLogger(MonopriceAudioConnector.class);
/** The output stream */
protected @Nullable OutputStream dataOut;
/** The input stream */
protected @Nullable InputStream dataIn;
/** true if the connection is established, false if not */
private boolean connected;
private @Nullable Thread readerThread;
private final List<MonopriceAudioMessageEventListener> listeners = new ArrayList<>();
/**
* Get whether the connection is established or not
*
* @return true if the connection is established
*/
public boolean isConnected() {
return connected;
}
/**
* Set whether the connection is established or not
*
* @param connected true if the connection is established
*/
protected void setConnected(boolean connected) {
this.connected = connected;
}
/**
* Set the thread that handles the feedback messages
*
* @param readerThread the thread
*/
protected void setReaderThread(Thread readerThread) {
this.readerThread = readerThread;
}
/**
* Open the connection with the MonopriceAudio device
*
* @throws MonopriceAudioException - In case of any problem
*/
public abstract void open() throws MonopriceAudioException;
/**
* Close the connection with the MonopriceAudio device
*/
public abstract void close();
/**
* Stop the thread that handles the feedback messages and close the opened input and output streams
*/
protected void cleanup() {
Thread readerThread = this.readerThread;
OutputStream dataOut = this.dataOut;
if (dataOut != null) {
try {
dataOut.close();
} catch (IOException e) {
logger.debug("Error closing dataOut: {}", e.getMessage());
}
this.dataOut = null;
}
InputStream dataIn = this.dataIn;
if (dataIn != null) {
try {
dataIn.close();
} catch (IOException e) {
logger.debug("Error closing dataIn: {}", e.getMessage());
}
this.dataIn = null;
}
if (readerThread != null) {
readerThread.interrupt();
try {
readerThread.join(3000);
} catch (InterruptedException e) {
logger.warn("Error joining readerThread: {}", e.getMessage());
}
this.readerThread = null;
}
}
/**
* Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
* actually read is returned as an integer.
*
* @param dataBuffer the buffer into which the data is read.
*
* @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
* stream has been reached.
*
* @throws MonopriceAudioException - If the input stream is null, if the first byte cannot be read for any reason
* other than the end of the file, if the input stream has been closed, or if some other I/O error
* occurs.
*/
protected int readInput(byte[] dataBuffer) throws MonopriceAudioException {
InputStream dataIn = this.dataIn;
if (dataIn == null) {
throw new MonopriceAudioException("readInput failed: input stream is null");
}
try {
return dataIn.read(dataBuffer);
} catch (IOException e) {
throw new MonopriceAudioException("readInput failed: " + e.getMessage(), e);
}
}
/**
* Get the status of a zone
*
* @param zone the zone to query for current status
*
* @throws MonopriceAudioException - In case of any problem
*/
public void queryZone(MonopriceAudioZone zone) throws MonopriceAudioException {
sendCommand(zone, MonopriceAudioCommand.QUERY, null);
}
/**
* Request the MonopriceAudio controller to execute a command
*
* @param zone the zone for which the command is to be run
* @param cmd the command to execute
* @param value the integer value to consider for volume, bass, treble, etc. adjustment
*
* @throws MonopriceAudioException - In case of any problem
*/
public void sendCommand(MonopriceAudioZone zone, MonopriceAudioCommand cmd, @Nullable Integer value)
throws MonopriceAudioException {
String messageStr = "";
if (cmd == MonopriceAudioCommand.QUERY) {
// query special case (ie: ? + zoneId)
messageStr = cmd.getValue() + zone.getZoneId();
} else if (value != null) {
// if the command passed a value, append it to the messageStr
messageStr = BEGIN_CMD + zone.getZoneId() + cmd.getValue() + String.format("%02d", value);
} else {
throw new MonopriceAudioException("Send command \"" + messageStr + "\" failed: passed in value is null");
}
messageStr += END_CMD;
logger.debug("Send command {}", messageStr);
OutputStream dataOut = this.dataOut;
if (dataOut == null) {
throw new MonopriceAudioException("Send command \"" + messageStr + "\" failed: output stream is null");
}
try {
dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
dataOut.flush();
} catch (IOException e) {
throw new MonopriceAudioException("Send command \"" + cmd.getValue() + "\" failed: " + e.getMessage(), e);
}
}
/**
* Add a listener to the list of listeners to be notified with events
*
* @param listener the listener
*/
public void addEventListener(MonopriceAudioMessageEventListener listener) {
listeners.add(listener);
}
/**
* Remove a listener from the list of listeners to be notified with events
*
* @param listener the listener
*/
public void removeEventListener(MonopriceAudioMessageEventListener listener) {
listeners.remove(listener);
}
/**
* Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
*
* @param incomingMessage the received message
*/
public void handleIncomingMessage(byte[] incomingMessage) {
String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
logger.debug("handleIncomingMessage: {}", message);
if (READ_ERROR.equals(message)) {
dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
return;
}
// Amp controller sends status string: #>1200010000130809100601
Matcher matcher = PATTERN.matcher(message);
if (matcher.find()) {
// pull out just the digits and send them as an event
dispatchKeyValue(KEY_ZONE_UPDATE, matcher.group(1));
} else {
logger.debug("no match on message: {}", message);
}
}
/**
* Dispatch an event (key, value) to the event listeners
*
* @param key the key
* @param value the value
*/
private void dispatchKeyValue(String key, String value) {
MonopriceAudioMessageEvent event = new MonopriceAudioMessageEvent(this, key, value);
listeners.forEach(l -> l.onNewMessageEvent(event));
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class to create a default MonopriceAudioConnector before initialization is complete.
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the MonopriceAudio binding
*/
@NonNullByDefault
public class MonopriceAudioDefaultConnector extends MonopriceAudioConnector {
private final Logger logger = LoggerFactory.getLogger(MonopriceAudioDefaultConnector.class);
@Override
public void open() throws MonopriceAudioException {
logger.warn(
"MonopriceAudio binding incorrectly configured. Please configure for Serial or IP over serial connection");
setConnected(false);
}
@Override
public void close() {
setConnected(false);
}
@Override
public void sendCommand(MonopriceAudioZone zone, MonopriceAudioCommand cmd, @Nullable Integer value) {
logger.warn(
"MonopriceAudio binding incorrectly configured. Please configure for Serial or IP over serial connection");
setConnected(false);
}
}

View File

@@ -0,0 +1,128 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class for communicating with the MonopriceAudio device through a serial over IP connection
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the MonopriceAudio binding
*/
@NonNullByDefault
public class MonopriceAudioIpConnector extends MonopriceAudioConnector {
private final Logger logger = LoggerFactory.getLogger(MonopriceAudioIpConnector.class);
private final @Nullable String address;
private final int port;
private final String uid;
private @Nullable Socket clientSocket;
/**
* Constructor
*
* @param address the IP address of the serial over IP device
* @param port the TCP port to be used
* @param uid the thing uid string
*/
public MonopriceAudioIpConnector(@Nullable String address, int port, String uid) {
this.address = address;
this.port = port;
this.uid = uid;
}
@Override
public synchronized void open() throws MonopriceAudioException {
logger.debug("Opening IP connection on IP {} port {}", this.address, this.port);
try {
Socket clientSocket = new Socket(this.address, this.port);
clientSocket.setSoTimeout(100);
dataOut = new DataOutputStream(clientSocket.getOutputStream());
dataIn = new DataInputStream(clientSocket.getInputStream());
Thread thread = new MonopriceAudioReaderThread(this, this.uid, this.address + "." + this.port);
setReaderThread(thread);
thread.start();
this.clientSocket = clientSocket;
setConnected(true);
logger.debug("IP connection opened");
} catch (IOException | SecurityException | IllegalArgumentException e) {
setConnected(false);
throw new MonopriceAudioException("Opening IP connection failed: " + e.getMessage(), e);
}
}
@Override
public synchronized void close() {
logger.debug("Closing IP connection");
super.cleanup();
Socket clientSocket = this.clientSocket;
if (clientSocket != null) {
try {
clientSocket.close();
} catch (IOException e) {
}
this.clientSocket = null;
}
setConnected(false);
logger.debug("IP connection closed");
}
/**
* Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
* actually read is returned as an integer.
* In case of socket timeout, the returned value is 0.
*
* @param dataBuffer the buffer into which the data is read.
*
* @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
* stream has been reached.
*
* @throws MonopriceAudioException - If the input stream is null, if the first byte cannot be read for any reason
* other than the end of the file, if the input stream has been closed, or if some other I/O error
* occurs.
*/
@Override
protected int readInput(byte[] dataBuffer) throws MonopriceAudioException {
InputStream dataIn = this.dataIn;
if (dataIn == null) {
throw new MonopriceAudioException("readInput failed: input stream is null");
}
try {
return dataIn.read(dataBuffer);
} catch (SocketTimeoutException e) {
return 0;
} catch (IOException e) {
throw new MonopriceAudioException("readInput failed: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import java.util.EventObject;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* MonopriceAudio event used to notify changes coming from messages received from the MonopriceAudio device
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class MonopriceAudioMessageEvent extends EventObject {
private static final long serialVersionUID = 1L;
private final String key;
private final String value;
public MonopriceAudioMessageEvent(Object source, String key, String value) {
super(source);
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import java.util.EventListener;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* MonopriceAudio Event Listener interface. Handles incoming MonopriceAudio message events
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public interface MonopriceAudioMessageEventListener extends EventListener {
/**
* Event handler method for incoming MonopriceAudio message events
*
* @param event the MonopriceAudioMessageEvent
*/
public void onNewMessageEvent(MonopriceAudioMessageEvent event);
}

View File

@@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A class that reads messages from the MonopriceAudio device in a dedicated thread
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the MonopriceAudio binding
*/
@NonNullByDefault
public class MonopriceAudioReaderThread extends Thread {
private static final int READ_BUFFER_SIZE = 16;
private static final int SIZE = 64;
private static final char TERM_CHAR = '\r';
private final Logger logger = LoggerFactory.getLogger(MonopriceAudioReaderThread.class);
private MonopriceAudioConnector connector;
/**
* Constructor
*
* @param connector the object that should handle the received message
* @param uid the thing uid string
* @param connectionId a string that uniquely identifies the particular connection
*/
public MonopriceAudioReaderThread(MonopriceAudioConnector connector, String uid, String connectionId) {
super("OH-binding-" + uid + "-" + connectionId);
this.connector = connector;
setDaemon(true);
}
@Override
public void run() {
logger.debug("Data listener started");
byte[] readDataBuffer = new byte[READ_BUFFER_SIZE];
byte[] dataBuffer = new byte[SIZE];
int index = 0;
try {
while (!Thread.interrupted()) {
int len = connector.readInput(readDataBuffer);
if (len > 0) {
for (int i = 0; i < len; i++) {
if (index < SIZE) {
dataBuffer[index++] = readDataBuffer[i];
}
if (readDataBuffer[i] == TERM_CHAR) {
if (index >= SIZE) {
dataBuffer[index - 1] = (byte) TERM_CHAR;
}
byte[] msg = Arrays.copyOf(dataBuffer, index);
connector.handleIncomingMessage(msg);
index = 0;
}
}
}
}
} catch (MonopriceAudioException e) {
logger.debug("Reading failed: {}", e.getMessage(), e);
connector.handleIncomingMessage(MonopriceAudioConnector.READ_ERROR.getBytes());
}
logger.debug("Data listener stopped");
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class for communicating with the MonopriceAudio device through a serial connection
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the MonopriceAudio binding
*/
@NonNullByDefault
public class MonopriceAudioSerialConnector extends MonopriceAudioConnector {
private final Logger logger = LoggerFactory.getLogger(MonopriceAudioSerialConnector.class);
private final String serialPortName;
private final SerialPortManager serialPortManager;
private final String uid;
private @Nullable SerialPort serialPort;
/**
* Constructor
*
* @param serialPortManager the serial port manager
* @param serialPortName the serial port name to be used
* @param uid the thing uid string
*/
public MonopriceAudioSerialConnector(SerialPortManager serialPortManager, String serialPortName, String uid) {
this.serialPortManager = serialPortManager;
this.serialPortName = serialPortName;
this.uid = uid;
}
@Override
public synchronized void open() throws MonopriceAudioException {
logger.debug("Opening serial connection on port {}", serialPortName);
try {
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
if (portIdentifier == null) {
setConnected(false);
throw new MonopriceAudioException("Opening serial connection failed: No Such Port");
}
SerialPort commPort = portIdentifier.open(this.getClass().getName(), 2000);
commPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
commPort.enableReceiveThreshold(1);
commPort.enableReceiveTimeout(100);
commPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
InputStream dataIn = commPort.getInputStream();
OutputStream dataOut = commPort.getOutputStream();
if (dataOut != null) {
dataOut.flush();
}
if (dataIn != null && dataIn.markSupported()) {
try {
dataIn.reset();
} catch (IOException e) {
}
}
Thread thread = new MonopriceAudioReaderThread(this, this.uid, this.serialPortName);
setReaderThread(thread);
thread.start();
this.serialPort = commPort;
this.dataIn = dataIn;
this.dataOut = dataOut;
setConnected(true);
logger.debug("Serial connection opened");
} catch (PortInUseException e) {
setConnected(false);
throw new MonopriceAudioException("Opening serial connection failed: Port in Use Exception", e);
} catch (UnsupportedCommOperationException e) {
setConnected(false);
throw new MonopriceAudioException("Opening serial connection failed: Unsupported Comm Operation Exception",
e);
} catch (UnsupportedEncodingException e) {
setConnected(false);
throw new MonopriceAudioException("Opening serial connection failed: Unsupported Encoding Exception", e);
} catch (IOException e) {
setConnected(false);
throw new MonopriceAudioException("Opening serial connection failed: IO Exception", e);
}
}
@Override
public synchronized void close() {
logger.debug("Closing serial connection");
SerialPort serialPort = this.serialPort;
if (serialPort != null) {
serialPort.removeEventListener();
}
super.cleanup();
if (serialPort != null) {
serialPort.close();
this.serialPort = null;
}
setConnected(false);
logger.debug("Serial connection closed");
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.communication;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
/**
* Represents the different internal zone IDs of the Monoprice Whole House Amplifier
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public enum MonopriceAudioZone {
ALL("all"),
ZONE1("11"),
ZONE2("12"),
ZONE3("13"),
ZONE4("14"),
ZONE5("15"),
ZONE6("16"),
ZONE7("21"),
ZONE8("22"),
ZONE9("23"),
ZONE10("24"),
ZONE11("25"),
ZONE12("26"),
ZONE13("31"),
ZONE14("32"),
ZONE15("33"),
ZONE16("34"),
ZONE17("35"),
ZONE18("36");
private final String zoneId;
// make a list of all valid zone names
public static final List<String> VALID_ZONES = Arrays.stream(values()).filter(z -> z != ALL)
.map(MonopriceAudioZone::name).collect(Collectors.toList());
// make a list of all valid zone ids
public static final List<String> VALID_ZONE_IDS = Arrays.stream(values()).filter(z -> z != ALL)
.map(MonopriceAudioZone::getZoneId).collect(Collectors.toList());
public static MonopriceAudioZone fromZoneId(String zoneId) throws MonopriceAudioException {
return Arrays.stream(values()).filter(z -> z.zoneId.equalsIgnoreCase(zoneId)).findFirst()
.orElseThrow(() -> new MonopriceAudioException("Invalid zoneId specified: " + zoneId));
}
MonopriceAudioZone(String zoneId) {
this.zoneId = zoneId;
}
/**
* Get the zone id
*
* @return the zone id
*/
public String getZoneId() {
return zoneId;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link MonopriceAudioThingConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class MonopriceAudioThingConfiguration {
public Integer numZones = 1;
public Integer pollingInterval = 15;
public @Nullable String serialPort;
public @Nullable String host;
public @Nullable Integer port;
public @Nullable String ignoreZones;
public Integer initialAllVolume = 1;
public @Nullable String inputLabel1;
public @Nullable String inputLabel2;
public @Nullable String inputLabel3;
public @Nullable String inputLabel4;
public @Nullable String inputLabel5;
public @Nullable String inputLabel6;
}

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.dto;
/**
* Represents the data elements of a single zone of the Monoprice Whole House Amplifier
*
* @author Michael Lobstein - Initial contribution
*/
public class MonopriceAudioZoneDTO {
private String zone;
private String page;
private String power;
private String mute;
private String dnd;
private int volume;
private int treble;
private int bass;
private int balance;
private String source;
private String keypad;
public void setZone(String zone) {
this.zone = zone;
}
public void setPage(String page) {
this.page = page;
}
public String getPage() {
return this.page;
}
public boolean isPageActive() {
return ("01").equals(this.page);
}
public void setPower(String power) {
this.power = power;
}
public String getPower() {
return this.power;
}
public boolean isPowerOn() {
return ("01").equals(this.power);
}
public void setMute(String mute) {
this.mute = mute;
}
public String getMute() {
return this.mute;
}
public boolean isMuted() {
return ("01").equals(this.mute);
}
public void setDnd(String dnd) {
this.dnd = dnd;
}
public String getDnd() {
return this.dnd;
}
public boolean isDndOn() {
return ("01").equals(this.dnd);
}
public int getVolume() {
return this.volume;
}
public void setVolume(int volume) {
this.volume = volume;
}
public int getTreble() {
return this.treble;
}
public void setTreble(int treble) {
this.treble = treble;
}
public int getBass() {
return this.bass;
}
public void setBass(int bass) {
this.bass = bass;
}
public int getBalance() {
return this.balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public String getSource() {
return this.source;
}
public void setSource(String source) {
this.source = source;
}
public void setKeypad(String keypad) {
this.keypad = keypad;
}
public String getKeypad() {
return this.keypad;
}
public boolean isKeypadActive() {
return ("01").equals(this.keypad);
}
@Override
public String toString() {
// Re-construct the original status message from the controller
// This is used to determine if something changed from the last polling update
return zone + page + power + mute + dnd + (String.format("%02d", volume)) + (String.format("%02d", treble))
+ (String.format("%02d", bass)) + (String.format("%02d", balance)) + source + keypad;
}
}

View File

@@ -0,0 +1,704 @@
/**
* Copyright (c) 2010-2020 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.monopriceaudio.internal.handler;
import static org.openhab.binding.monopriceaudio.internal.MonopriceAudioBindingConstants.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.monopriceaudio.internal.MonopriceAudioException;
import org.openhab.binding.monopriceaudio.internal.MonopriceAudioStateDescriptionOptionProvider;
import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioCommand;
import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioConnector;
import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioDefaultConnector;
import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioIpConnector;
import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioMessageEvent;
import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioMessageEventListener;
import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioSerialConnector;
import org.openhab.binding.monopriceaudio.internal.communication.MonopriceAudioZone;
import org.openhab.binding.monopriceaudio.internal.configuration.MonopriceAudioThingConfiguration;
import org.openhab.binding.monopriceaudio.internal.dto.MonopriceAudioZoneDTO;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Channel;
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.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MonopriceAudioHandler} is responsible for handling commands, which are sent to one of the channels.
*
* Based on the Rotel binding by Laurent Garnier
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class MonopriceAudioHandler extends BaseThingHandler implements MonopriceAudioMessageEventListener {
private static final long RECON_POLLING_INTERVAL_SEC = 60;
private static final long INITIAL_POLLING_DELAY_SEC = 5;
private static final Pattern PATTERN = Pattern
.compile("^(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})");
private static final String ZONE = "ZONE";
private static final String ALL = "all";
private static final String CHANNEL_DELIMIT = "#";
private static final String ON_STR = "01";
private static final String OFF_STR = "00";
private static final int ONE = 1;
private static final int MAX_ZONES = 18;
private static final int MAX_SRC = 6;
private static final int MIN_VOLUME = 0;
private static final int MAX_VOLUME = 38;
private static final int MIN_TONE = -7;
private static final int MAX_TONE = 7;
private static final int MIN_BALANCE = -10;
private static final int MAX_BALANCE = 10;
private static final int BALANCE_OFFSET = 10;
private static final int TONE_OFFSET = 7;
// build a Map with a MonopriceAudioZoneDTO for each zoneId
private final Map<String, MonopriceAudioZoneDTO> zoneDataMap = MonopriceAudioZone.VALID_ZONE_IDS.stream()
.collect(Collectors.toMap(s -> s, s -> new MonopriceAudioZoneDTO()));
private final Logger logger = LoggerFactory.getLogger(MonopriceAudioHandler.class);
private final MonopriceAudioStateDescriptionOptionProvider stateDescriptionProvider;
private final SerialPortManager serialPortManager;
private @Nullable ScheduledFuture<?> reconnectJob;
private @Nullable ScheduledFuture<?> pollingJob;
private MonopriceAudioConnector connector = new MonopriceAudioDefaultConnector();
private Set<String> ignoreZones = new HashSet<>();
private long lastPollingUpdate = System.currentTimeMillis();
private long pollingInterval = 0;
private int numZones = 0;
private int allVolume = 1;
private int initialAllVolume = 0;
private Object sequenceLock = new Object();
public MonopriceAudioHandler(Thing thing, MonopriceAudioStateDescriptionOptionProvider stateDescriptionProvider,
SerialPortManager serialPortManager) {
super(thing);
this.stateDescriptionProvider = stateDescriptionProvider;
this.serialPortManager = serialPortManager;
}
@Override
public void initialize() {
final String uid = this.getThing().getUID().getAsString();
MonopriceAudioThingConfiguration config = getConfigAs(MonopriceAudioThingConfiguration.class);
final String serialPort = config.serialPort;
final String host = config.host;
final Integer port = config.port;
final String ignoreZonesConfig = config.ignoreZones;
// Check configuration settings
String configError = null;
if ((serialPort == null || serialPort.isEmpty()) && (host == null || host.isEmpty())) {
configError = "undefined serialPort and host configuration settings; please set one of them";
} else if (serialPort != null && (host == null || host.isEmpty())) {
if (serialPort.toLowerCase().startsWith("rfc2217")) {
configError = "use host and port configuration settings for a serial over IP connection";
}
} else {
if (port == null) {
configError = "undefined port configuration setting";
} else if (port <= 0) {
configError = "invalid port configuration setting";
}
}
if (configError != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
return;
}
if (serialPort != null) {
connector = new MonopriceAudioSerialConnector(serialPortManager, serialPort, uid);
} else if (port != null) {
connector = new MonopriceAudioIpConnector(host, port, uid);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Either Serial port or Host & Port must be specifed");
return;
}
pollingInterval = config.pollingInterval;
numZones = config.numZones;
initialAllVolume = config.initialAllVolume;
// If zones were specified to be ignored by the 'all*' commands, use the specified binding
// zone ids to get the controller's internal zone ids and save those to a list
if (ignoreZonesConfig != null) {
for (String zone : ignoreZonesConfig.split(",")) {
try {
int zoneInt = Integer.parseInt(zone);
if (zoneInt >= ONE && zoneInt <= MAX_ZONES) {
ignoreZones.add(ZONE + zoneInt);
} else {
logger.warn("Invalid ignore zone value: {}, value must be between {} and {}", zone, ONE,
MAX_ZONES);
}
} catch (NumberFormatException nfe) {
logger.warn("Invalid ignore zone value: {}", zone);
}
}
}
// Build a state option list for the source labels
List<StateOption> sourcesLabels = new ArrayList<>();
sourcesLabels.add(new StateOption("1", config.inputLabel1));
sourcesLabels.add(new StateOption("2", config.inputLabel2));
sourcesLabels.add(new StateOption("3", config.inputLabel3));
sourcesLabels.add(new StateOption("4", config.inputLabel4));
sourcesLabels.add(new StateOption("5", config.inputLabel5));
sourcesLabels.add(new StateOption("6", config.inputLabel6));
// Put the source labels on all active zones
List<Integer> activeZones = IntStream.range(1, numZones + 1).boxed().collect(Collectors.toList());
stateDescriptionProvider.setStateOptions(
new ChannelUID(getThing().getUID(), ALL + CHANNEL_DELIMIT + CHANNEL_TYPE_ALLSOURCE), sourcesLabels);
activeZones.forEach(zoneNum -> {
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(),
ZONE.toLowerCase() + zoneNum + CHANNEL_DELIMIT + CHANNEL_TYPE_SOURCE), sourcesLabels);
});
// remove the channels for the zones we are not using
if (numZones < MAX_ZONES) {
List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
List<Integer> zonesToRemove = IntStream.range(numZones + 1, MAX_ZONES + 1).boxed()
.collect(Collectors.toList());
zonesToRemove.forEach(zone -> {
channels.removeIf(c -> (c.getUID().getId().contains(ZONE.toLowerCase() + zone)));
});
updateThing(editThing().withChannels(channels).build());
}
// initialize the all volume state
allVolume = initialAllVolume;
long allVolumePct = Math
.round((double) (initialAllVolume - MIN_VOLUME) / (double) (MAX_VOLUME - MIN_VOLUME) * 100.0);
updateState(ALL + CHANNEL_DELIMIT + CHANNEL_TYPE_ALLVOLUME, new PercentType(BigDecimal.valueOf(allVolumePct)));
scheduleReconnectJob();
schedulePollingJob();
updateStatus(ThingStatus.UNKNOWN);
}
@Override
public void dispose() {
cancelReconnectJob();
cancelPollingJob();
closeConnection();
ignoreZones.clear();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channel = channelUID.getId();
String[] channelSplit = channel.split(CHANNEL_DELIMIT);
MonopriceAudioZone zone = MonopriceAudioZone.valueOf(channelSplit[0].toUpperCase());
String channelType = channelSplit[1];
if (getThing().getStatus() != ThingStatus.ONLINE) {
logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
return;
}
boolean success = true;
synchronized (sequenceLock) {
if (!connector.isConnected()) {
logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
return;
}
if (command instanceof RefreshType) {
updateChannelState(zone, channelType, zoneDataMap.get(zone.getZoneId()));
return;
}
Stream<String> zoneStream = MonopriceAudioZone.VALID_ZONES.stream().limit(numZones);
try {
switch (channelType) {
case CHANNEL_TYPE_POWER:
if (command instanceof OnOffType) {
connector.sendCommand(zone, MonopriceAudioCommand.POWER, command == OnOffType.ON ? 1 : 0);
zoneDataMap.get(zone.getZoneId()).setPower(command == OnOffType.ON ? ON_STR : OFF_STR);
}
break;
case CHANNEL_TYPE_SOURCE:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= ONE && value <= MAX_SRC) {
logger.debug("Got source command {} zone {}", value, zone);
connector.sendCommand(zone, MonopriceAudioCommand.SOURCE, value);
zoneDataMap.get(zone.getZoneId()).setSource(String.format("%02d", value));
}
}
break;
case CHANNEL_TYPE_VOLUME:
if (command instanceof PercentType) {
int value = (int) Math
.round(((PercentType) command).doubleValue() / 100.0 * (MAX_VOLUME - MIN_VOLUME))
+ MIN_VOLUME;
logger.debug("Got volume command {} zone {}", value, zone);
connector.sendCommand(zone, MonopriceAudioCommand.VOLUME, value);
zoneDataMap.get(zone.getZoneId()).setVolume(value);
}
break;
case CHANNEL_TYPE_MUTE:
if (command instanceof OnOffType) {
connector.sendCommand(zone, MonopriceAudioCommand.MUTE, command == OnOffType.ON ? 1 : 0);
zoneDataMap.get(zone.getZoneId()).setMute(command == OnOffType.ON ? ON_STR : OFF_STR);
}
break;
case CHANNEL_TYPE_TREBLE:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= MIN_TONE && value <= MAX_TONE) {
logger.debug("Got treble command {} zone {}", value, zone);
connector.sendCommand(zone, MonopriceAudioCommand.TREBLE, value + TONE_OFFSET);
zoneDataMap.get(zone.getZoneId()).setTreble(value + TONE_OFFSET);
}
}
break;
case CHANNEL_TYPE_BASS:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= MIN_TONE && value <= MAX_TONE) {
logger.debug("Got bass command {} zone {}", value, zone);
connector.sendCommand(zone, MonopriceAudioCommand.BASS, value + TONE_OFFSET);
zoneDataMap.get(zone.getZoneId()).setBass(value + TONE_OFFSET);
}
}
break;
case CHANNEL_TYPE_BALANCE:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= MIN_BALANCE && value <= MAX_BALANCE) {
logger.debug("Got balance command {} zone {}", value, zone);
connector.sendCommand(zone, MonopriceAudioCommand.BALANCE, value + BALANCE_OFFSET);
zoneDataMap.get(zone.getZoneId()).setBalance(value + BALANCE_OFFSET);
}
}
break;
case CHANNEL_TYPE_DND:
if (command instanceof OnOffType) {
connector.sendCommand(zone, MonopriceAudioCommand.DND, command == OnOffType.ON ? 1 : 0);
zoneDataMap.get(zone.getZoneId()).setDnd(command == OnOffType.ON ? ON_STR : OFF_STR);
}
break;
case CHANNEL_TYPE_ALLPOWER:
if (command instanceof OnOffType) {
zoneStream.forEach((zoneName) -> {
if (command == OnOffType.OFF || !ignoreZones.contains(zoneName)) {
try {
connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
MonopriceAudioCommand.POWER, command == OnOffType.ON ? 1 : 0);
if (command == OnOffType.ON) {
// reset the volume of each zone to allVolume
connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
MonopriceAudioCommand.VOLUME, allVolume);
}
} catch (MonopriceAudioException e) {
logger.warn("Error Turning All Zones On: {}", e.getMessage());
}
}
});
}
break;
case CHANNEL_TYPE_ALLSOURCE:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= ONE && value <= MAX_SRC) {
zoneStream.forEach((zoneName) -> {
if (!ignoreZones.contains(zoneName)) {
try {
connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
MonopriceAudioCommand.SOURCE, value);
} catch (MonopriceAudioException e) {
logger.warn("Error Setting Source for All Zones: {}", e.getMessage());
}
}
});
}
}
break;
case CHANNEL_TYPE_ALLVOLUME:
if (command instanceof PercentType) {
int value = (int) Math
.round(((PercentType) command).doubleValue() / 100.0 * (MAX_VOLUME - MIN_VOLUME))
+ MIN_VOLUME;
allVolume = value;
zoneStream.forEach((zoneName) -> {
if (!ignoreZones.contains(zoneName)) {
try {
connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
MonopriceAudioCommand.VOLUME, value);
} catch (MonopriceAudioException e) {
logger.warn("Error Setting Volume for All Zones: {}", e.getMessage());
}
}
});
}
break;
case CHANNEL_TYPE_ALLMUTE:
if (command instanceof OnOffType) {
int cmd = command == OnOffType.ON ? 1 : 0;
zoneStream.forEach((zoneName) -> {
if (!ignoreZones.contains(zoneName)) {
try {
connector.sendCommand(MonopriceAudioZone.valueOf(zoneName),
MonopriceAudioCommand.MUTE, cmd);
} catch (MonopriceAudioException e) {
logger.warn("Error Setting Mute for All Zones: {}", e.getMessage());
}
}
});
}
break;
default:
success = false;
logger.debug("Command {} from channel {} failed: unexpected command", command, channel);
break;
}
if (success) {
logger.trace("Command {} from channel {} succeeded", command, channel);
}
} catch (MonopriceAudioException e) {
logger.warn("Command {} from channel {} failed: {}", command, channel, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
closeConnection();
scheduleReconnectJob();
}
}
}
/**
* Open the connection with the MonopriceAudio device
*
* @return true if the connection is opened successfully or false if not
*/
private synchronized boolean openConnection() {
connector.addEventListener(this);
try {
connector.open();
} catch (MonopriceAudioException e) {
logger.debug("openConnection() failed: {}", e.getMessage());
}
logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
return connector.isConnected();
}
/**
* Close the connection with the MonopriceAudio device
*/
private synchronized void closeConnection() {
if (connector.isConnected()) {
connector.close();
connector.removeEventListener(this);
logger.debug("closeConnection(): disconnected");
}
}
@Override
public void onNewMessageEvent(MonopriceAudioMessageEvent evt) {
String key = evt.getKey();
String updateData = evt.getValue().trim();
if (!MonopriceAudioConnector.KEY_ERROR.equals(key)) {
updateStatus(ThingStatus.ONLINE);
}
try {
switch (key) {
case MonopriceAudioConnector.KEY_ERROR:
logger.debug("Reading feedback message failed");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Reading thread ended");
closeConnection();
break;
case MonopriceAudioConnector.KEY_ZONE_UPDATE:
String zoneId = updateData.substring(0, 2);
if (MonopriceAudioZone.VALID_ZONE_IDS.contains(zoneId)) {
MonopriceAudioZone targetZone = MonopriceAudioZone.fromZoneId(zoneId);
processZoneUpdate(targetZone, zoneDataMap.get(zoneId), updateData);
} else {
logger.warn("invalid event: {} for key: {}", evt.getValue(), key);
}
break;
default:
logger.debug("onNewMessageEvent: unhandled key {}", key);
break;
}
} catch (NumberFormatException e) {
logger.warn("Invalid value {} for key {}", updateData, key);
} catch (MonopriceAudioException e) {
logger.warn("Error processing zone update: {}", e.getMessage());
}
}
/**
* Schedule the reconnection job
*/
private void scheduleReconnectJob() {
logger.debug("Schedule reconnect job");
cancelReconnectJob();
reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
synchronized (sequenceLock) {
if (!connector.isConnected()) {
logger.debug("Trying to reconnect...");
closeConnection();
String error = null;
if (openConnection()) {
try {
long prevUpdateTime = lastPollingUpdate;
connector.queryZone(MonopriceAudioZone.ZONE1);
// prevUpdateTime should have changed if a zone update was received
if (lastPollingUpdate == prevUpdateTime) {
error = "Controller not responding to status requests";
}
} catch (MonopriceAudioException e) {
error = "First command after connection failed";
logger.warn("{}: {}", error, e.getMessage());
closeConnection();
}
} else {
error = "Reconnection failed";
}
if (error != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
} else {
updateStatus(ThingStatus.ONLINE);
lastPollingUpdate = System.currentTimeMillis();
}
}
}
}, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
}
/**
* Cancel the reconnection job
*/
private void cancelReconnectJob() {
ScheduledFuture<?> reconnectJob = this.reconnectJob;
if (reconnectJob != null) {
reconnectJob.cancel(true);
this.reconnectJob = null;
}
}
/**
* Schedule the polling job
*/
private void schedulePollingJob() {
logger.debug("Schedule polling job");
cancelPollingJob();
pollingJob = scheduler.scheduleWithFixedDelay(() -> {
synchronized (sequenceLock) {
if (connector.isConnected()) {
logger.debug("Polling the controller for updated status...");
// poll each zone up to the number of zones specified in the configuration
MonopriceAudioZone.VALID_ZONES.stream().limit(numZones).forEach((zoneName) -> {
try {
connector.queryZone(MonopriceAudioZone.valueOf(zoneName));
} catch (MonopriceAudioException e) {
logger.warn("Polling error: {}", e.getMessage());
}
});
// if the last successful polling update was more than 2.25 intervals ago, the controller
// is either switched off or not responding even though the connection is still good
if ((System.currentTimeMillis() - lastPollingUpdate) > (pollingInterval * 2.25 * 1000)) {
logger.warn("Controller not responding to status requests");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Controller not responding to status requests");
closeConnection();
scheduleReconnectJob();
}
}
}
}, INITIAL_POLLING_DELAY_SEC, pollingInterval, TimeUnit.SECONDS);
}
/**
* Cancel the polling job
*/
private void cancelPollingJob() {
ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
}
/**
* Update the state of a channel
*
* @param channel the channel
*/
private void updateChannelState(MonopriceAudioZone zone, String channelType, MonopriceAudioZoneDTO zoneData) {
String channel = zone.name().toLowerCase() + CHANNEL_DELIMIT + channelType;
if (!isLinked(channel)) {
return;
}
State state = UnDefType.UNDEF;
switch (channelType) {
case CHANNEL_TYPE_POWER:
state = zoneData.isPowerOn() ? OnOffType.ON : OnOffType.OFF;
break;
case CHANNEL_TYPE_SOURCE:
state = new DecimalType(zoneData.getSource());
break;
case CHANNEL_TYPE_VOLUME:
long volumePct = Math.round(
(double) (zoneData.getVolume() - MIN_VOLUME) / (double) (MAX_VOLUME - MIN_VOLUME) * 100.0);
state = new PercentType(BigDecimal.valueOf(volumePct));
break;
case CHANNEL_TYPE_MUTE:
state = zoneData.isMuted() ? OnOffType.ON : OnOffType.OFF;
break;
case CHANNEL_TYPE_TREBLE:
state = new DecimalType(BigDecimal.valueOf(zoneData.getTreble() - TONE_OFFSET));
break;
case CHANNEL_TYPE_BASS:
state = new DecimalType(BigDecimal.valueOf(zoneData.getBass() - TONE_OFFSET));
break;
case CHANNEL_TYPE_BALANCE:
state = new DecimalType(BigDecimal.valueOf(zoneData.getBalance() - BALANCE_OFFSET));
break;
case CHANNEL_TYPE_DND:
state = zoneData.isDndOn() ? OnOffType.ON : OnOffType.OFF;
break;
case CHANNEL_TYPE_PAGE:
state = zoneData.isPageActive() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
break;
case CHANNEL_TYPE_KEYPAD:
state = zoneData.isKeypadActive() ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
break;
default:
break;
}
updateState(channel, state);
}
private void processZoneUpdate(MonopriceAudioZone zone, MonopriceAudioZoneDTO zoneData, String newZoneData) {
// only process the update if something actually changed in this zone since the last time through
if (!newZoneData.equals(zoneData.toString())) {
// example status string: 1200010000130809100601, matcher pattern from above:
// "^(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})"
Matcher matcher = PATTERN.matcher(newZoneData);
if (matcher.find()) {
zoneData.setZone(matcher.group(1));
if (!matcher.group(2).equals(zoneData.getPage())) {
zoneData.setPage(matcher.group(2));
updateChannelState(zone, CHANNEL_TYPE_PAGE, zoneData);
}
if (!matcher.group(3).equals(zoneData.getPower())) {
zoneData.setPower(matcher.group(3));
updateChannelState(zone, CHANNEL_TYPE_POWER, zoneData);
}
if (!matcher.group(4).equals(zoneData.getMute())) {
zoneData.setMute(matcher.group(4));
updateChannelState(zone, CHANNEL_TYPE_MUTE, zoneData);
}
if (!matcher.group(5).equals(zoneData.getDnd())) {
zoneData.setDnd(matcher.group(5));
updateChannelState(zone, CHANNEL_TYPE_DND, zoneData);
}
int volume = Integer.parseInt(matcher.group(6));
if (volume != zoneData.getVolume()) {
zoneData.setVolume(volume);
updateChannelState(zone, CHANNEL_TYPE_VOLUME, zoneData);
}
int treble = Integer.parseInt(matcher.group(7));
if (treble != zoneData.getTreble()) {
zoneData.setTreble(treble);
updateChannelState(zone, CHANNEL_TYPE_TREBLE, zoneData);
}
int bass = Integer.parseInt(matcher.group(8));
if (bass != zoneData.getBass()) {
zoneData.setBass(bass);
updateChannelState(zone, CHANNEL_TYPE_BASS, zoneData);
}
int balance = Integer.parseInt(matcher.group(9));
if (balance != zoneData.getBalance()) {
zoneData.setBalance(balance);
updateChannelState(zone, CHANNEL_TYPE_BALANCE, zoneData);
}
if (!matcher.group(10).equals(zoneData.getSource())) {
zoneData.setSource(matcher.group(10));
updateChannelState(zone, CHANNEL_TYPE_SOURCE, zoneData);
}
if (!matcher.group(11).equals(zoneData.getKeypad())) {
zoneData.setKeypad(matcher.group(11));
updateChannelState(zone, CHANNEL_TYPE_KEYPAD, zoneData);
}
} else {
logger.debug("Invalid zone update message: {}", newZoneData);
}
}
lastPollingUpdate = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="monopriceaudio" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Monoprice Whole House Audio Binding</name>
<description>Controls the Monoprice MPR-SG6Z or Dayton Audio DAX66 Whole House Amplifier.</description>
<author>Michael Lobstein</author>
</binding:binding>

View File

@@ -0,0 +1,256 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="monopriceaudio"
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">
<!-- Monoprice Whole House Amplifier Thing -->
<thing-type id="amplifier">
<label>Whole House Amplifier</label>
<description>
A Multi-zone Whole House Amplifier System
</description>
<channel-groups>
<channel-group id="all" typeId="all">
<label>All Zones</label>
<description>Control All Zones Simultaneously</description>
</channel-group>
<channel-group id="zone1" typeId="zone">
<label>Zone 1</label>
<description>The Controls for Zone 1</description>
</channel-group>
<channel-group id="zone2" typeId="zone">
<label>Zone 2</label>
<description>The Controls for Zone 2</description>
</channel-group>
<channel-group id="zone3" typeId="zone">
<label>Zone 3</label>
<description>The Controls for Zone 3</description>
</channel-group>
<channel-group id="zone4" typeId="zone">
<label>Zone 4</label>
<description>The Controls for Zone 4</description>
</channel-group>
<channel-group id="zone5" typeId="zone">
<label>Zone 5</label>
<description>The Controls for Zone 5</description>
</channel-group>
<channel-group id="zone6" typeId="zone">
<label>Zone 6</label>
<description>The Controls for Zone 6</description>
</channel-group>
<channel-group id="zone7" typeId="zone">
<label>Zone 7</label>
<description>The Controls for Zone 7</description>
</channel-group>
<channel-group id="zone8" typeId="zone">
<label>Zone 8</label>
<description>The Controls for Zone 8</description>
</channel-group>
<channel-group id="zone9" typeId="zone">
<label>Zone 9</label>
<description>The Controls for Zone 9</description>
</channel-group>
<channel-group id="zone10" typeId="zone">
<label>Zone 10</label>
<description>The Controls for Zone 10</description>
</channel-group>
<channel-group id="zone11" typeId="zone">
<label>Zone 11</label>
<description>The Controls for Zone 11</description>
</channel-group>
<channel-group id="zone12" typeId="zone">
<label>Zone 12</label>
<description>The Controls for Zone 12</description>
</channel-group>
<channel-group id="zone13" typeId="zone">
<label>Zone 13</label>
<description>The Controls for Zone 13</description>
</channel-group>
<channel-group id="zone14" typeId="zone">
<label>Zone 14</label>
<description>The Controls for Zone 14</description>
</channel-group>
<channel-group id="zone15" typeId="zone">
<label>Zone 15</label>
<description>The Controls for Zone 15</description>
</channel-group>
<channel-group id="zone16" typeId="zone">
<label>Zone 16</label>
<description>The Controls for Zone 16</description>
</channel-group>
<channel-group id="zone17" typeId="zone">
<label>Zone 17</label>
<description>The Controls for Zone 17</description>
</channel-group>
<channel-group id="zone18" typeId="zone">
<label>Zone 18</label>
<description>The Controls for Zone 18</description>
</channel-group>
</channel-groups>
<config-description>
<parameter name="serialPort" type="text" required="false">
<context>serial-port</context>
<label>Serial Port</label>
<description>Serial Port to Use for Connecting to the Monoprice Amplifier</description>
</parameter>
<parameter name="host" type="text" required="false">
<context>network-address</context>
<label>Address</label>
<description>Host Name or IP Address of the Machine Connected to the Monoprice Amplifier (Serial over IP)</description>
</parameter>
<parameter name="port" type="integer" min="1" max="65535" required="false">
<label>Port</label>
<description>Communication Port (IP or Serial over IP). For IP connection to the Monoprice Amplifier</description>
<default>4444</default>
</parameter>
<parameter name="numZones" type="integer" min="1" max="18" required="true">
<label>Number of Zones</label>
<description>Number of Zones on the Amplifier to Utilize in the Binding (Up to 18 Zones With 3 Amplifiers Connected
Together)</description>
<default>6</default>
</parameter>
<parameter name="pollingInterval" type="integer" min="5" max="60" unit="s" required="false">
<label>Polling Interval</label>
<description>Configures How Often to Poll the Controller to Check for Zone Updates (5-60; Default 15)</description>
<default>15</default>
</parameter>
<parameter name="ignoreZones" type="text" required="false">
<label>Ignore Zones</label>
<description>(Optional) A Comma Seperated List of Zone Numbers That Will Ignore the 'All Zone' (Except All Off)
Commands (ie: 1,6,10)</description>
</parameter>
<parameter name="initialAllVolume" type="integer" min="1" max="30" required="false">
<label>Initial All Volume</label>
<description>When 'All' Zones Are Activated, the Volume Will Reset to This Value (1-30; default 10) to Prevent
Excessive Blaring of Sound ;)</description>
<default>10</default>
</parameter>
<parameter name="inputLabel1" type="text" required="false">
<label>Source 1 Input Label</label>
<description>Friendly Name for the Input Source to Be Displayed in the UI (ie: Chromecast, Radio, CD, etc.)</description>
<default>Source 1</default>
</parameter>
<parameter name="inputLabel2" type="text" required="false">
<label>Source 2 Input Label</label>
<description>Friendly Name for the Input Source to Be Displayed in the UI (ie: Chromecast, Radio, CD, etc.)</description>
<default>Source 2</default>
</parameter>
<parameter name="inputLabel3" type="text" required="false">
<label>Source 3 Input Label</label>
<description>Friendly Name for the Input Source to Be Displayed in the UI (ie: Chromecast, Radio, CD, etc.)</description>
<default>Source 3</default>
</parameter>
<parameter name="inputLabel4" type="text" required="false">
<label>Source 4 Input Label</label>
<description>Friendly Name for the Input Source to Be Displayed in the UI (ie: Chromecast, Radio, CD, etc.)</description>
<default>Source 4</default>
</parameter>
<parameter name="inputLabel5" type="text" required="false">
<label>Source 5 Input Label</label>
<description>Friendly Name for the Input Source to Be Displayed in the UI (ie: Chromecast, Radio, CD, etc.)</description>
<default>Source 5</default>
</parameter>
<parameter name="inputLabel6" type="text" required="false">
<label>Source 6 Input Label</label>
<description>Friendly Name for the Input Source to Be Displayed in the UI (ie: Chromecast, Radio, CD, etc.)</description>
<default>Source 6</default>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="all">
<label>All Zones</label>
<description>Control All Zones Simultaneously</description>
<channels>
<channel id="allpower" typeId="allpower"/>
<channel id="allsource" typeId="source"/>
<channel id="allvolume" typeId="system.volume"/>
<channel id="allmute" typeId="system.mute"/>
</channels>
</channel-group-type>
<channel-group-type id="zone">
<label>Zone Controls</label>
<description>The Controls for the Zone</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="source" typeId="source"/>
<channel id="volume" typeId="system.volume"/>
<channel id="mute" typeId="system.mute"/>
<channel id="treble" typeId="treble"/>
<channel id="bass" typeId="bass"/>
<channel id="balance" typeId="balance"/>
<channel id="dnd" typeId="dnd"/>
<channel id="page" typeId="page"/>
<channel id="keypad" typeId="keypad"/>
</channels>
</channel-group-type>
<channel-type id="allpower">
<item-type>Switch</item-type>
<label>All On</label>
<description>Turn All Zones On or Off</description>
</channel-type>
<channel-type id="source">
<item-type>Number</item-type>
<label>Source Input</label>
<description>Select the Source Input</description>
<state min="1" max="6"/>
</channel-type>
<channel-type id="treble">
<item-type>Number</item-type>
<label>Treble Adjustment</label>
<description>Adjust the Treble</description>
<state min="-7" max="7" step="1" pattern="%d"/>
</channel-type>
<channel-type id="bass">
<item-type>Number</item-type>
<label>Bass Adjustment</label>
<description>Adjust the Bass</description>
<state min="-7" max="7" step="1" pattern="%d"/>
</channel-type>
<channel-type id="balance">
<item-type>Number</item-type>
<label>Balance Adjustment</label>
<description>Adjust the Balance</description>
<state min="-10" max="10" step="1" pattern="%d"/>
</channel-type>
<channel-type id="dnd">
<item-type>Switch</item-type>
<label>Do Not Disturb</label>
<description>Controls if the Zone Should Ignore an Incoming Audio Page</description>
</channel-type>
<channel-type id="page">
<item-type>Contact</item-type>
<label>Page Active</label>
<description>Indicates if the Page Mode is Active for This Zone</description>
<state readOnly="true">
<options>
<option value="CLOSED">Inactive</option>
<option value="OPEN">Active</option>
</options>
</state>
</channel-type>
<channel-type id="keypad">
<item-type>Contact</item-type>
<label>Keypad Connected</label>
<description>Indicates if a Physical Keypad is Attached to This Zone</description>
<state readOnly="true">
<options>
<option value="CLOSED">Disconnected</option>
<option value="OPEN">Connected</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>