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