[benqprojector] Migrate benqprojector binding to OH3 (#10341)

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein
2021-06-12 05:00:08 -05:00
committed by GitHub
parent c12e189a13
commit e452de8e15
21 changed files with 1597 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.benqprojector-${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-benqprojector" description="BenQ Projector 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.benqprojector/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BenqProjectorBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class BenqProjectorBindingConstants {
private static final String BINDING_ID = "benqprojector";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_PROJECTOR_SERIAL = new ThingTypeUID(BINDING_ID, "projector-serial");
public static final ThingTypeUID THING_TYPE_PROJECTOR_TCP = new ThingTypeUID(BINDING_ID, "projector-tcp");
// Some Channel types
public static final String CHANNEL_TYPE_POWER = "power";
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception for BenQ projector command errors.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class BenqProjectorCommandException extends Exception {
private static final long serialVersionUID = -8048415193494625295L;
public BenqProjectorCommandException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal;
import java.io.InvalidClassException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem;
/**
* Represents all valid command types which could be processed by this
* binding.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public enum BenqProjectorCommandType {
POWER("Power", SwitchItem.class),
SOURCE("Source", StringItem.class),
PICTURE_MODE("PictureMode", StringItem.class),
ASPECT_RATIO("AspectRatio", StringItem.class),
FREEZE("Freeze", SwitchItem.class),
BLANK("Blank", SwitchItem.class),
DIRECTCMD("DirectCmd", StringItem.class),
LAMP_TIME("LampTime", NumberItem.class);
private final String text;
private Class<? extends Item> itemClass;
private BenqProjectorCommandType(final String text, Class<? extends Item> itemClass) {
this.text = text;
this.itemClass = itemClass;
}
@Override
public String toString() {
return text;
}
public Class<? extends Item> getItemClass() {
return itemClass;
}
/**
* Procedure to validate command type string.
*
* @param commandTypeText
* command string e.g. RawData, Command, Brightness
* @return true if item is valid.
* @throws IllegalArgumentException
* Not valid command type.
* @throws InvalidClassException
* Not valid class for command type.
*/
public static boolean validateBinding(String commandTypeText, Class<? extends Item> itemClass)
throws IllegalArgumentException, InvalidClassException {
for (BenqProjectorCommandType c : BenqProjectorCommandType.values()) {
if (c.text.equalsIgnoreCase(commandTypeText)) {
if (c.getItemClass().equals(itemClass)) {
return true;
} else {
throw new InvalidClassException("Not valid class for command type");
}
}
}
throw new IllegalArgumentException("Not valid command type");
}
/**
* Procedure to convert command type string to command type class.
*
* @param commandTypeText
* command string e.g. RawData, Command, Brightness
* @return corresponding command type.
* @throws InvalidClassException
* Not valid class for command type.
*/
public static BenqProjectorCommandType getCommandType(String commandTypeText) throws IllegalArgumentException {
for (BenqProjectorCommandType c : BenqProjectorCommandType.values()) {
if (c.text.equalsIgnoreCase(commandTypeText)) {
return c;
}
}
throw new IllegalArgumentException("Not valid command type");
}
}

View File

@@ -0,0 +1,213 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.benqprojector.internal.configuration.BenqProjectorConfiguration;
import org.openhab.binding.benqprojector.internal.connector.BenqProjectorConnector;
import org.openhab.binding.benqprojector.internal.connector.BenqProjectorSerialConnector;
import org.openhab.binding.benqprojector.internal.connector.BenqProjectorTcpConnector;
import org.openhab.binding.benqprojector.internal.enums.Switch;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provide high level interface to BenQ projector.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class BenqProjectorDevice {
private static final String UNSUPPORTED_ITM = "Unsupported item";
private static final String BLOCK_ITM = "Block item";
private static final String ILLEGAL_FMT = "Illegal format";
private static final int LAMP_REFRESH_WAIT_MINUTES = 5;
private ExpiringCache<Integer> cachedLampHours = new ExpiringCache<>(Duration.ofMinutes(LAMP_REFRESH_WAIT_MINUTES),
this::queryLamp);
private final Logger logger = LoggerFactory.getLogger(BenqProjectorDevice.class);
private BenqProjectorConnector connection;
private boolean connected = false;
public BenqProjectorDevice(SerialPortManager serialPortManager, BenqProjectorConfiguration config) {
connection = new BenqProjectorSerialConnector(serialPortManager, config.serialPort);
}
public BenqProjectorDevice(BenqProjectorConfiguration config) {
connection = new BenqProjectorTcpConnector(config.host, config.port);
}
private synchronized String sendQuery(String query) throws BenqProjectorCommandException, BenqProjectorException {
logger.debug("Query: '{}'", query);
String response = connection.sendMessage(query);
if (response.length() == 0) {
throw new BenqProjectorException("No response received");
}
if (response.contains(UNSUPPORTED_ITM)) {
return "UNSUPPORTED";
}
if (response.contains(BLOCK_ITM)) {
throw new BenqProjectorCommandException("Block Item received for command: " + query);
}
if (response.contains(ILLEGAL_FMT)) {
throw new BenqProjectorCommandException("Illegal Format response received for command: " + query);
}
logger.debug("Response: '{}'", response);
// example: SOUR=HDMI2
String[] responseParts = response.split("=");
if (responseParts.length != 2) {
throw new BenqProjectorCommandException("Invalid respose for command: " + query);
}
return responseParts[1].toLowerCase();
}
protected void sendCommand(String command) throws BenqProjectorCommandException, BenqProjectorException {
sendQuery(command);
}
protected int queryInt(String query) throws BenqProjectorCommandException, BenqProjectorException {
String response = sendQuery(query);
return Integer.parseInt(response);
}
protected String queryString(String query) throws BenqProjectorCommandException, BenqProjectorException {
return sendQuery(query);
}
public void connect() throws BenqProjectorException {
connection.connect();
connected = true;
}
public void disconnect() throws BenqProjectorException {
connection.disconnect();
connected = false;
}
public boolean isConnected() {
return connected;
}
/*
* Power
*/
public Switch getPowerStatus() throws BenqProjectorCommandException, BenqProjectorException {
return (queryString("pow=?").contains("on") ? Switch.ON : Switch.OFF);
}
public void setPower(Switch value) throws BenqProjectorCommandException, BenqProjectorException {
sendCommand(value == Switch.ON ? "pow=on" : "pow=off");
}
/*
* Source
*/
public @Nullable String getSource() throws BenqProjectorCommandException, BenqProjectorException {
return queryString("sour=?");
}
public void setSource(String value) throws BenqProjectorCommandException, BenqProjectorException {
sendCommand(String.format("sour=%s", value));
}
/*
* Picture Mode
*/
public @Nullable String getPictureMode() throws BenqProjectorCommandException, BenqProjectorException {
return queryString("appmod=?");
}
public void setPictureMode(String value) throws BenqProjectorCommandException, BenqProjectorException {
sendCommand(String.format("appmod=%s", value));
}
/*
* Aspect Ratio
*/
public @Nullable String getAspectRatio() throws BenqProjectorCommandException, BenqProjectorException {
return queryString("asp=?");
}
public void setAspectRatio(String value) throws BenqProjectorCommandException, BenqProjectorException {
sendCommand(String.format("asp=%s", value));
}
/*
* Blank Screen
*/
public Switch getBlank() throws BenqProjectorCommandException, BenqProjectorException {
return (queryString("blank=?").contains("on") ? Switch.ON : Switch.OFF);
}
public void setBlank(Switch value) throws BenqProjectorCommandException, BenqProjectorException {
sendCommand(String.format("blank=%s", (value == Switch.ON ? "on" : "off")));
}
/*
* Freeze
*/
public Switch getFreeze() throws BenqProjectorCommandException, BenqProjectorException {
return (queryString("freeze=?").contains("on") ? Switch.ON : Switch.OFF);
}
public void setFreeze(Switch value) throws BenqProjectorCommandException, BenqProjectorException {
sendCommand(String.format("freeze=%s", (value == Switch.ON ? "on" : "off")));
}
/*
* Direct Command
*/
public void sendDirectCommand(String value) throws BenqProjectorCommandException, BenqProjectorException {
sendCommand(value);
}
/*
* Lamp Time (hours) - get from cache
*/
public int getLampTime() throws BenqProjectorCommandException, BenqProjectorException {
Integer lampHours = cachedLampHours.getValue();
if (lampHours != null) {
return lampHours.intValue();
} else {
throw new BenqProjectorCommandException("cachedLampHours returned null");
}
}
/*
* Get Lamp Time
*/
private @Nullable Integer queryLamp() {
try {
return Integer.valueOf(queryInt("ltim=?"));
} catch (BenqProjectorCommandException | BenqProjectorException e) {
logger.debug("Error executing command ltim=?", e);
return null;
}
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception for BenQ projector errors.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class BenqProjectorException extends Exception {
private static final long serialVersionUID = -8048415193494625295L;
public BenqProjectorException(String message) {
super(message);
}
public BenqProjectorException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal;
import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.benqprojector.internal.handler.BenqProjectorHandler;
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 BenqProjectorHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.benqprojector", service = ThingHandlerFactory.class)
public class BenqProjectorHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PROJECTOR_SERIAL,
THING_TYPE_PROJECTOR_TCP);
private final SerialPortManager serialPortManager;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Activate
public BenqProjectorHandlerFactory(final @Reference SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_PROJECTOR_SERIAL.equals(thingTypeUID) || THING_TYPE_PROJECTOR_TCP.equals(thingTypeUID)) {
return new BenqProjectorHandler(thing, serialPortManager);
}
return null;
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link BenqProjectorConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class BenqProjectorConfiguration {
/**
* Serial port used for communication.
*/
public String serialPort = "";
/**
* Host or IP address used for communication over a TCP link (if serialPort is not set).
*/
public String host = "";
/**
* Port used for communication over a TCP link (if serialPort is not set).
*/
public int port;
/**
* Polling interval to refresh states.
*/
public int pollingInterval;
}

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal.connector;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.benqprojector.internal.BenqProjectorException;
/**
* Base class for BenQ projector communication.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public interface BenqProjectorConnector {
public static final int TIMEOUT_MS = 5 * 1000;
public static final String START = "\r*";
public static final String END = "#\r";
public static final String BLANK = "";
/**
* Procedure for connecting to projector.
*
* @throws BenqProjectorException
*/
void connect() throws BenqProjectorException;
/**
* Procedure for disconnecting to projector controller.
*
* @throws BenqProjectorException
*/
void disconnect() throws BenqProjectorException;
/**
* Procedure for sending raw data to projector.
*
* @param data
* Message to send.
*
* @throws BenqProjectorException
*/
String sendMessage(String data) throws BenqProjectorException;
/**
* Common method called by the Serial or Tcp connector to send the message to the projector, wait for a response and
* return it after processing.
*
* @param data
* Message to send.
* @param in
* The connector's input stream.
* @param out
* The connector's output stream.
*
* @throws BenqProjectorException
*/
default String sendMsgReadResp(String data, @Nullable InputStream in, @Nullable OutputStream out)
throws IOException, BenqProjectorException {
String resp = BLANK;
if (in != null && out != null) {
out.write((START + data + END).getBytes(StandardCharsets.US_ASCII));
out.flush();
long startTime = System.currentTimeMillis();
long elapsedTime = 0;
while (elapsedTime < TIMEOUT_MS) {
int availableBytes = in.available();
if (availableBytes > 0) {
byte[] tmpData = new byte[availableBytes];
int readBytes = in.read(tmpData, 0, availableBytes);
resp = resp.concat(new String(tmpData, 0, readBytes, StandardCharsets.US_ASCII));
if (resp.contains(END)) {
return resp.replaceAll("[\\r\\n*#>]", BLANK).replace(data, BLANK);
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new BenqProjectorException(e);
}
}
elapsedTime = System.currentTimeMillis() - startTime;
}
}
return resp;
}
}

View File

@@ -0,0 +1,168 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal.connector;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.benqprojector.internal.BenqProjectorException;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortEvent;
import org.openhab.core.io.transport.serial.SerialPortEventListener;
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;
/**
* Connector for serial port communication.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class BenqProjectorSerialConnector implements BenqProjectorConnector, SerialPortEventListener {
private final Logger logger = LoggerFactory.getLogger(BenqProjectorSerialConnector.class);
private final String serialPortName;
private final SerialPortManager serialPortManager;
private @Nullable InputStream in = null;
private @Nullable OutputStream out = null;
private @Nullable SerialPort serialPort = null;
public BenqProjectorSerialConnector(SerialPortManager serialPortManager, String serialPort) {
this.serialPortManager = serialPortManager;
this.serialPortName = serialPort;
}
@Override
public void connect() throws BenqProjectorException {
try {
logger.debug("Open connection to serial port '{}'", serialPortName);
SerialPortIdentifier serialPortIdentifier = serialPortManager.getIdentifier(serialPortName);
if (serialPortIdentifier == null) {
throw new IOException("Unknown serial port");
}
SerialPort serialPort = serialPortIdentifier.open(this.getClass().getName(), 2000);
serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
serialPort.enableReceiveThreshold(1);
serialPort.disableReceiveTimeout();
InputStream in = serialPort.getInputStream();
OutputStream out = serialPort.getOutputStream();
if (in != null && out != null) {
out.flush();
if (in.markSupported()) {
in.reset();
}
serialPort.notifyOnDataAvailable(true);
this.serialPort = serialPort;
this.in = in;
this.out = out;
}
} catch (PortInUseException | UnsupportedCommOperationException | IOException e) {
throw new BenqProjectorException(e);
}
}
@Override
public void disconnect() throws BenqProjectorException {
InputStream in = this.in;
OutputStream out = this.out;
SerialPort serialPort = this.serialPort;
if (out != null) {
logger.debug("Close serial out stream");
try {
out.close();
} catch (IOException e) {
logger.debug("Error occurred when closing serial out stream: {}", e.getMessage());
}
this.out = null;
}
if (in != null) {
logger.debug("Close serial in stream");
try {
in.close();
} catch (IOException e) {
logger.debug("Error occurred when closing serial in stream: {}", e.getMessage());
}
this.in = null;
}
if (serialPort != null) {
logger.debug("Close serial port");
serialPort.close();
serialPort.removeEventListener();
this.serialPort = null;
}
logger.debug("Closed");
}
@Override
public String sendMessage(String data) throws BenqProjectorException {
InputStream in = this.in;
OutputStream out = this.out;
if (in == null || out == null) {
connect();
in = this.in;
out = this.out;
}
try {
if (in != null && out != null) {
// flush input stream
if (in.markSupported()) {
in.reset();
} else {
while (in.available() > 0) {
int availableBytes = in.available();
if (availableBytes > 0) {
byte[] tmpData = new byte[availableBytes];
in.read(tmpData, 0, availableBytes);
}
}
}
return sendMsgReadResp(data, in, out);
} else {
return BLANK;
}
} catch (IOException e) {
logger.debug("IO error occurred...reconnect and resend once: {}", e.getMessage());
disconnect();
connect();
try {
return sendMsgReadResp(data, in, out);
} catch (IOException e1) {
throw new BenqProjectorException(e);
}
}
}
@Override
public void serialEvent(SerialPortEvent arg0) {
}
}

View File

@@ -0,0 +1,143 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal.connector;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.benqprojector.internal.BenqProjectorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Connector for TCP communication.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class BenqProjectorTcpConnector implements BenqProjectorConnector {
private final Logger logger = LoggerFactory.getLogger(BenqProjectorTcpConnector.class);
private final String ip;
private final int port;
private @Nullable Socket socket = null;
private @Nullable InputStream in = null;
private @Nullable OutputStream out = null;
public BenqProjectorTcpConnector(String ip, int port) {
this.ip = ip;
this.port = port;
}
@Override
public void connect() throws BenqProjectorException {
logger.debug("Open connection to address'{}:{}'", ip, port);
try {
Socket socket = new Socket(ip, port);
this.socket = socket;
in = socket.getInputStream();
out = socket.getOutputStream();
} catch (IOException e) {
throw new BenqProjectorException(e);
}
}
@Override
public void disconnect() throws BenqProjectorException {
OutputStream out = this.out;
if (out != null) {
logger.debug("Close tcp out stream");
try {
out.close();
} catch (IOException e) {
logger.debug("Error occurred when closing tcp out stream: {}", e.getMessage());
}
}
InputStream in = this.in;
if (in != null) {
logger.debug("Close tcp in stream");
try {
in.close();
} catch (IOException e) {
logger.debug("Error occurred when closing tcp in stream: {}", e.getMessage());
}
}
Socket socket = this.socket;
if (socket != null) {
logger.debug("Closing socket");
try {
socket.close();
} catch (IOException e) {
logger.debug("Error occurred when closing tcp socket: {}", e.getMessage());
}
}
this.socket = null;
this.out = null;
this.in = null;
logger.debug("Closed");
}
@Override
public String sendMessage(String data) throws BenqProjectorException {
InputStream in = this.in;
OutputStream out = this.out;
if (in == null || out == null) {
connect();
in = this.in;
out = this.out;
}
try {
if (in != null) {
// flush input stream
if (in.markSupported()) {
in.reset();
} else {
while (in.available() > 0) {
int availableBytes = in.available();
if (availableBytes > 0) {
byte[] tmpData = new byte[availableBytes];
in.read(tmpData, 0, availableBytes);
}
}
}
return sendMsgReadResp(data, in, out);
} else {
return BLANK;
}
} catch (IOException e) {
logger.debug("IO error occurred...reconnect and resend once: {}", e.getMessage());
disconnect();
connect();
try {
return sendMsgReadResp(data, in, out);
} catch (IOException e1) {
throw new BenqProjectorException(e);
}
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal.enums;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Valid values for BenQ switch commands.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public enum Switch {
ON,
OFF;
}

View File

@@ -0,0 +1,291 @@
/**
* Copyright (c) 2010-2021 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.benqprojector.internal.handler;
import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.*;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.benqprojector.internal.BenqProjectorCommandException;
import org.openhab.binding.benqprojector.internal.BenqProjectorCommandType;
import org.openhab.binding.benqprojector.internal.BenqProjectorDevice;
import org.openhab.binding.benqprojector.internal.BenqProjectorException;
import org.openhab.binding.benqprojector.internal.configuration.BenqProjectorConfiguration;
import org.openhab.binding.benqprojector.internal.enums.Switch;
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.StringType;
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.ThingTypeUID;
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.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BenqProjectorHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* Based on 'epsonprojector' originally by Pauli Anttila & Yannick Schaus
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class BenqProjectorHandler extends BaseThingHandler {
private static final int DEFAULT_POLLING_INTERVAL_SEC = 10;
private final Logger logger = LoggerFactory.getLogger(BenqProjectorHandler.class);
private final SerialPortManager serialPortManager;
private @Nullable ScheduledFuture<?> pollingJob;
private Optional<BenqProjectorDevice> device = Optional.empty();
private boolean isPowerOn = false;
private int pollingInterval = DEFAULT_POLLING_INTERVAL_SEC;
public BenqProjectorHandler(Thing thing, SerialPortManager serialPortManager) {
super(thing);
this.serialPortManager = serialPortManager;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelId = channelUID.getId();
if (command instanceof RefreshType) {
Channel channel = this.thing.getChannel(channelUID);
if (channel != null && getThing().getStatus() == ThingStatus.ONLINE) {
updateChannelState(channel);
}
} else {
BenqProjectorCommandType benqCommand = BenqProjectorCommandType.getCommandType(channelId);
sendDataToDevice(benqCommand, command);
}
}
@Override
public void initialize() {
BenqProjectorConfiguration config = getConfigAs(BenqProjectorConfiguration.class);
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_PROJECTOR_SERIAL.equals(thingTypeUID)) {
device = Optional.of(new BenqProjectorDevice(serialPortManager, config));
} else if (THING_TYPE_PROJECTOR_TCP.equals(thingTypeUID)) {
device = Optional.of(new BenqProjectorDevice(config));
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
return;
}
pollingInterval = config.pollingInterval;
updateStatus(ThingStatus.UNKNOWN);
schedulePollingJob();
}
/**
* Schedule the polling job
*/
private void schedulePollingJob() {
cancelPollingJob();
pollingJob = scheduler.scheduleWithFixedDelay(() -> {
List<Channel> channels = this.thing.getChannels();
for (Channel channel : channels) {
// only query power when projector is off
if (isPowerOn || channel.getUID().getId().equals(CHANNEL_TYPE_POWER)) {
updateChannelState(channel);
}
}
}, 0, (pollingInterval > 0) ? pollingInterval : DEFAULT_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
}
/**
* Cancel the polling job
*/
private void cancelPollingJob() {
ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
}
@Override
public void dispose() {
cancelPollingJob();
closeConnection();
super.dispose();
}
private void updateChannelState(Channel channel) {
try {
if (!isLinked(channel.getUID()) && !channel.getUID().getId().equals(CHANNEL_TYPE_POWER)) {
return;
}
BenqProjectorCommandType benqCommand = BenqProjectorCommandType.getCommandType(channel.getUID().getId());
State state = queryDataFromDevice(benqCommand);
if (state != null) {
if (isLinked(channel.getUID())) {
updateState(channel.getUID(), state);
}
// the first valid response will cause the thing to go ONLINE
if (state != UnDefType.UNDEF) {
updateStatus(ThingStatus.ONLINE);
}
}
} catch (IllegalArgumentException e) {
logger.warn("Unknown channel {}", channel.getUID().getId());
}
}
@Nullable
private State queryDataFromDevice(BenqProjectorCommandType commandType) {
BenqProjectorDevice remoteController = device.get();
try {
if (!remoteController.isConnected()) {
remoteController.connect();
}
switch (commandType) {
case POWER:
Switch powerStatus = remoteController.getPowerStatus();
if (powerStatus == Switch.ON) {
isPowerOn = true;
return OnOffType.ON;
} else {
isPowerOn = false;
return OnOffType.OFF;
}
case SOURCE:
String source = remoteController.getSource();
if (source != null) {
return new StringType(source);
} else {
return UnDefType.UNDEF;
}
case PICTURE_MODE:
String picturemode = remoteController.getPictureMode();
if (picturemode != null) {
return new StringType(picturemode);
} else {
return UnDefType.UNDEF;
}
case ASPECT_RATIO:
String aspectratio = remoteController.getAspectRatio();
if (aspectratio != null) {
return new StringType(aspectratio);
} else {
return UnDefType.UNDEF;
}
case FREEZE:
Switch freeze = remoteController.getFreeze();
return freeze == Switch.ON ? OnOffType.ON : OnOffType.OFF;
case BLANK:
Switch blank = remoteController.getBlank();
return blank == Switch.ON ? OnOffType.ON : OnOffType.OFF;
case DIRECTCMD:
break;
case LAMP_TIME:
int lampTime = remoteController.getLampTime();
return new DecimalType(lampTime);
default:
logger.warn("Unknown '{}' command!", commandType);
return UnDefType.UNDEF;
}
} catch (BenqProjectorCommandException e) {
logger.debug("Error executing command '{}', {}", commandType, e.getMessage());
return UnDefType.UNDEF;
} catch (BenqProjectorException e) {
logger.debug("Couldn't execute command '{}', {}", commandType, e.getMessage());
closeConnection();
return null;
}
return UnDefType.UNDEF;
}
private void sendDataToDevice(BenqProjectorCommandType commandType, Command command) {
BenqProjectorDevice remoteController = device.get();
try {
if (!remoteController.isConnected()) {
remoteController.connect();
}
switch (commandType) {
case POWER:
if (command == OnOffType.ON) {
remoteController.setPower(Switch.ON);
isPowerOn = true;
} else {
remoteController.setPower(Switch.OFF);
isPowerOn = false;
}
break;
case SOURCE:
remoteController.setSource(command.toString());
break;
case PICTURE_MODE:
remoteController.setPictureMode(command.toString());
break;
case ASPECT_RATIO:
remoteController.setAspectRatio(command.toString());
break;
case FREEZE:
remoteController.setFreeze(command == OnOffType.ON ? Switch.ON : Switch.OFF);
break;
case BLANK:
remoteController.setBlank(command == OnOffType.ON ? Switch.ON : Switch.OFF);
break;
case DIRECTCMD:
remoteController.sendDirectCommand(command.toString());
break;
default:
logger.warn("Unknown '{}' command!", commandType);
break;
}
} catch (BenqProjectorCommandException e) {
logger.debug("Error executing command '{}', {}", commandType, e.getMessage());
} catch (BenqProjectorException e) {
logger.warn("Couldn't execute command '{}', {}", commandType, e.getMessage());
closeConnection();
}
}
private void closeConnection() {
BenqProjectorDevice remoteController = device.get();
try {
logger.debug("Closing connection to device '{}'", this.thing.getUID());
remoteController.disconnect();
updateStatus(ThingStatus.OFFLINE);
} catch (BenqProjectorException e) {
logger.debug("Error occurred when closing connection to device '{}'", this.thing.getUID(), e);
}
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="benqprojector" 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>BenQ Projector Binding</name>
<description>This binding is compatible with BenQ projectors</description>
</binding:binding>

View File

@@ -0,0 +1,168 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="benqprojector"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="projector-serial">
<label>BenQ Projector - Serial</label>
<description>A BenQ projector connected via a serial port</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="source" typeId="source"/>
<channel id="picturemode" typeId="picturemode"/>
<channel id="aspectratio" typeId="aspectratio"/>
<channel id="freeze" typeId="freeze"/>
<channel id="blank" typeId="blank"/>
<channel id="directcmd" typeId="directcmd"/>
<channel id="lamptime" typeId="lamptime"/>
</channels>
<config-description>
<parameter name="serialPort" type="text" required="true">
<label>Serial Port</label>
<context>serial-port</context>
<description>Serial Port to Use for Connecting to the BenQ Projector</description>
</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 Projector for Updates (5-60; Default 10)</description>
<default>10</default>
</parameter>
</config-description>
</thing-type>
<thing-type id="projector-tcp">
<label>BenQ Projector - TCP/IP</label>
<description>A BenQ projector connected via the built-in ethernet port or a serial over
IP device</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="source" typeId="source"/>
<channel id="picturemode" typeId="picturemode"/>
<channel id="aspectratio" typeId="aspectratio"/>
<channel id="freeze" typeId="freeze"/>
<channel id="blank" typeId="blank"/>
<channel id="directcmd" typeId="directcmd"/>
<channel id="lamptime" typeId="lamptime"/>
</channels>
<config-description>
<parameter name="host" type="text" required="true">
<label>Host</label>
<context>network-address</context>
<description>IP address for the projector or serial over IP device</description>
</parameter>
<parameter name="port" type="integer" min="1" max="65535" required="true">
<label>Port</label>
<description>Port for the projector or serial over IP device</description>
<default>8000</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 Projector for Updates (5-60; Default 10)</description>
<default>10</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="source">
<item-type>String</item-type>
<label>Source</label>
<description>Retrieve or Set the Input Source</description>
<state>
<options>
<option value="hdmi">HDMI</option>
<option value="hdmi2">HDMI2</option>
<option value="ypbr">Component</option>
<option value="rgb">Computer/YPbPr</option>
<option value="rgb2">Computer/YPbPr2</option>
<option value="vid">Video</option>
<option value="svid">S-Video</option>
</options>
</state>
</channel-type>
<channel-type id="picturemode">
<item-type>String</item-type>
<label>Picture Mode</label>
<description>Retrieve or Set the Picture Mode</description>
<state>
<options>
<option value="dynamic">Dynamic</option>
<option value="preset">Presentation</option>
<option value="srgb">sRGB</option>
<option value="bright">Bright</option>
<option value="livingroom">Living Room</option>
<option value="game">Game</option>
<option value="cine">Cinema</option>
<option value="std">Standard/Vivid</option>
<option value="football">Football</option>
<option value="footballbt">Football Bright</option>
<option value="user1">User 1</option>
<option value="user2">User 2</option>
<option value="user3">User 3</option>
<option value="isfday">ISF Day</option>
<option value="isfnight">ISF Night</option>
<option value="threed">3-D</option>
</options>
</state>
</channel-type>
<channel-type id="aspectratio">
<item-type>String</item-type>
<label>Aspect Ratio</label>
<description>Retrieve or Set the Aspect Ratio</description>
<state>
<options>
<option value="4:3">4:3</option>
<option value="16:9">16:9</option>
<option value="auto">Auto</option>
<option value="lbox">Letterbox</option>
<option value="wide">Wide</option>
</options>
</state>
</channel-type>
<channel-type id="freeze">
<item-type>Switch</item-type>
<label>Freeze Image</label>
<description>Turn the Freeze Image Mode On or Off</description>
</channel-type>
<channel-type id="blank">
<item-type>Switch</item-type>
<label>Screen Blank</label>
<description>Turn the Screen Blank On or Off</description>
</channel-type>
<channel-type id="directcmd" advanced="true">
<item-type>String</item-type>
<label>Direct Command</label>
<description>Send a Command Directly to the Projector</description>
<state>
<options>
<option value="mute=on">Mute On</option>
<option value="mute=off">Mute Off</option>
<option value="vol=+">Volume +</option>
<option value="vol=-">Volume -</option>
<option value="zoomI">Zoom In</option>
<option value="zoomO">Zoom Out</option>
<option value="auto">Zoom Auto</option>
<option value="menu=on">Menu On</option>
<option value="menu=off">Menu Off</option>
<option value="up">Up</option>
<option value="down">Down</option>
<option value="left">Left</option>
<option value="right">Right</option>
<option value="enter">Enter</option>
</options>
</state>
</channel-type>
<channel-type id="lamptime">
<item-type>Number</item-type>
<label>Lamp Time</label>
<description>Retrieves the Lamp Hours</description>
<state readOnly="true" pattern="%d h"/>
</channel-type>
</thing:thing-descriptions>