added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user