added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.coolmasternet/.classpath
Normal file
32
bundles/org.openhab.binding.coolmasternet/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.coolmasternet/.project
Normal file
23
bundles/org.openhab.binding.coolmasternet/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.coolmasternet</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.binding.coolmasternet/NOTICE
Normal file
13
bundles/org.openhab.binding.coolmasternet/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
43
bundles/org.openhab.binding.coolmasternet/README.md
Normal file
43
bundles/org.openhab.binding.coolmasternet/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# CoolMasterNet Binding
|
||||
|
||||
The CoolMasterNet binding is used to control [CoolMasterNet HVAC bridge devices](https://coolautomation.com/products/coolmasternet/), using the "ASCII I/F" plaintext TCP control protocol.
|
||||
|
||||
## Discovery
|
||||
|
||||
The CoolMasterNet protocol does not support automatic discovery.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
- `controller` is a openHAB "bridge", and represents a single CoolMasterNet device. A single controller supports one or more HVAC units.
|
||||
- `hvac` is an HVAC device connected to a controller. Each `hvac` thing is identified by a CoolMasterNet UID (refer to CoolMasterNet controller documentation).
|
||||
|
||||
Example demo.things configuration for two HVAC devices connected to a CoolMasterNet device found at IP 192.168.0.100:
|
||||
|
||||
```perl
|
||||
Bridge coolmasternet:controller:main [ host="192.168.0.100" ] {
|
||||
Thing hvac a [ uid="L1.100" ]
|
||||
Thing hvac b [ uid="L1.101" ]
|
||||
}
|
||||
```
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel | Item Type | Description |
|
||||
|--------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| on | Switch | Turn HVAC unit on and off. |
|
||||
| mode | String | HVAC mode (cool, heat, auto, dry, fan). Unit may not support all modes. |
|
||||
| fan_speed | String | Fan speed (l, m, h, t, a ) for respectively "Low", "Medium", "High", "Top" or "Auto". Unit may not support all speeds. |
|
||||
| set_temp | Number | Temperature target setpoint in Celsius. |
|
||||
| current_temp | Number | Current temperature in Celsius at HVAC unit. |
|
||||
| louvre | String | Louvre angle (0, a, h, 3, 4, 6, v) for respectively "No Control", "Auto Swing", "Horizontal", "30 degrees", "45 degrees", "60 degrees" or "Vertical". Unit may not support all angles. |
|
||||
|
||||
## Item Configuration
|
||||
|
||||
```java
|
||||
Switch ACOn "Lounge AC ON/OFF" { channel="coolmasternet:hvac:main:a:on"}
|
||||
String ACMode "Lounge AC Mode" { channel="coolmasternet:hvac:main:a:mode" }
|
||||
Number ACTemp "Lounge Temp" { channel="coolmasternet:hvac:main:a:current_temp" }
|
||||
Number ACSet "Lounge AC Set" { channel="coolmasternet:hvac:main:a:set_temp" }
|
||||
String ACFan "Lounge AC Fan" { channel="coolmasternet:hvac:main:a:fan_speed" }
|
||||
String ACLouvre "Lounge AC Louvre" { channel="coolmasternet:hvac:main:a:louvre_angle" }
|
||||
```
|
||||
17
bundles/org.openhab.binding.coolmasternet/pom.xml
Normal file
17
bundles/org.openhab.binding.coolmasternet/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.coolmasternet</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: CoolMasterNet Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.coolmasternet-${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-coolmasternet" description="CoolMasterNet Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.coolmasternet/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 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.coolmasternet.internal;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.coolmasternet.internal.config.ControllerConfiguration;
|
||||
import org.openhab.binding.coolmasternet.internal.handler.HVACHandler;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
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.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Bridge to access a CoolMasterNet unit's ASCII protocol via TCP socket.
|
||||
*
|
||||
* <p>
|
||||
* A single CoolMasterNet can be connected to one or more HVAC units, each with
|
||||
* a unique UID. Each HVAC is an individual thing inside the bridge.
|
||||
*
|
||||
* @author Angus Gratton - Initial contribution
|
||||
* @author Wouter Born - Fix null pointer exceptions and stop refresh job on update/dispose
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class ControllerHandler extends BaseBridgeHandler {
|
||||
private static final String LF = "\n";
|
||||
private static final byte PROMPT = ">".getBytes(US_ASCII)[0];
|
||||
private static final int LS_LINE_LENGTH = 36;
|
||||
private static final int LS_LINE_TEMP_SCALE_OFFSET = 13;
|
||||
private static final int MAX_VALID_LINE_LENGTH = LS_LINE_LENGTH * 20;
|
||||
private static final int SINK_TIMEOUT_MS = 25;
|
||||
private static final int SOCKET_TIMEOUT_MS = 2000;
|
||||
|
||||
private ControllerConfiguration cfg = new ControllerConfiguration();
|
||||
private Unit<?> unit = SIUnits.CELSIUS;
|
||||
private final Logger logger = LoggerFactory.getLogger(ControllerHandler.class);
|
||||
private final Object socketLock = new Object();
|
||||
|
||||
private @Nullable ScheduledFuture<?> poller;
|
||||
private @Nullable Socket socket;
|
||||
|
||||
public ControllerHandler(final Bridge thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
cfg = getConfigAs(ControllerConfiguration.class);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
determineTemperatureUnits();
|
||||
stopPoller();
|
||||
startPoller();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
stopPoller();
|
||||
disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the temperature unit configured for this controller.
|
||||
*
|
||||
* <p>
|
||||
* CoolMasterNet defaults to Celsius, but allows a user to change the scale
|
||||
* on a per-controller basis using the ASCII I/F "set deg" command. Given
|
||||
* changing the unit is very rarely performed, there is no direct support
|
||||
* for doing so within this binding.
|
||||
*
|
||||
* @return the unit as determined from the first line of the "ls" command
|
||||
*/
|
||||
public Unit<?> getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
private void determineTemperatureUnits() {
|
||||
synchronized (socketLock) {
|
||||
try {
|
||||
checkConnection();
|
||||
final String ls = sendCommand("ls");
|
||||
if (ls.length() < LS_LINE_LENGTH) {
|
||||
throw new CoolMasterClientError("Invalid 'ls' response: '%s'", ls);
|
||||
}
|
||||
final char scale = ls.charAt(LS_LINE_TEMP_SCALE_OFFSET);
|
||||
unit = scale == 'C' ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT;
|
||||
logger.trace("Temperature scale '{}' set to {}", scale, unit);
|
||||
} catch (final IOException ioe) {
|
||||
logger.warn("Could not determine temperature scale", ioe);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ioe.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startPoller() {
|
||||
synchronized (scheduler) {
|
||||
logger.debug("Scheduling new poller");
|
||||
poller = scheduler.scheduleWithFixedDelay(this::poll, 0, cfg.refresh, SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopPoller() {
|
||||
synchronized (scheduler) {
|
||||
final ScheduledFuture<?> poller = this.poller;
|
||||
if (poller != null) {
|
||||
logger.debug("Cancelling existing poller");
|
||||
poller.cancel(true);
|
||||
this.poller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void poll() {
|
||||
try {
|
||||
checkConnection();
|
||||
} catch (final IOException ioe) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ioe.getMessage());
|
||||
return;
|
||||
}
|
||||
for (Thing t : getThing().getThings()) {
|
||||
final HVACHandler h = (HVACHandler) t.getHandler();
|
||||
if (h != null) {
|
||||
h.refresh();
|
||||
}
|
||||
}
|
||||
if (isConnected()) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passively determine if the client socket appears to be connected, but do
|
||||
* modify the connection state.
|
||||
*
|
||||
* <p>
|
||||
* Use {@link #checkConnection()} if active verification (and potential
|
||||
* reconnection) of the CoolNetMaster connection is required.
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
synchronized (socketLock) {
|
||||
final Socket socket = this.socket;
|
||||
return socket != null && socket.isConnected() && !socket.isClosed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a specific ASCII I/F command to CoolMasterNet and return its response.
|
||||
*
|
||||
* <p>
|
||||
* This method automatically acquires a connection.
|
||||
*
|
||||
* @return the server response to the command (never empty)
|
||||
* @throws {@link IOException} if communications failed with the server
|
||||
*/
|
||||
public String sendCommand(final String command) throws IOException {
|
||||
synchronized (socketLock) {
|
||||
checkConnection();
|
||||
|
||||
final StringBuilder response = new StringBuilder();
|
||||
try {
|
||||
final Socket socket = this.socket;
|
||||
if (socket == null || !isConnected()) {
|
||||
throw new CoolMasterClientError(String.format("No connection for sending command %s", command));
|
||||
}
|
||||
|
||||
logger.trace("Sending command '{}'", command);
|
||||
final Writer out = new OutputStreamWriter(socket.getOutputStream(), US_ASCII);
|
||||
out.write(command);
|
||||
out.write(LF);
|
||||
out.flush();
|
||||
|
||||
final Reader isr = new InputStreamReader(socket.getInputStream(), US_ASCII);
|
||||
final BufferedReader in = new BufferedReader(isr);
|
||||
while (true) {
|
||||
String line = in.readLine();
|
||||
logger.trace("Read result '{}'", line);
|
||||
if (line == null || "OK".equals(line)) {
|
||||
return response.toString();
|
||||
}
|
||||
response.append(line);
|
||||
if (response.length() > MAX_VALID_LINE_LENGTH) {
|
||||
throw new CoolMasterClientError("Command '%s' received unexpected response '%s'", command,
|
||||
response);
|
||||
}
|
||||
}
|
||||
} catch (final SocketTimeoutException ste) {
|
||||
if (response.length() == 0) {
|
||||
throw new CoolMasterClientError("Command '%s' received no response", command);
|
||||
}
|
||||
throw new CoolMasterClientError("Command '%s' received truncated response '%s'", command, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a client socket is connected and ready to receive commands.
|
||||
*
|
||||
* <p>
|
||||
* This method may block for up to {@link #SOCKET_TIMEOUT_MS}, depending on
|
||||
* the state of the connection. This usual time is {@link #SINK_TIMEOUT_MS}.
|
||||
*
|
||||
* <p>
|
||||
* Return of this method guarantees the socket is ready to receive a
|
||||
* command. If the socket could not be made ready, an exception is raised.
|
||||
*
|
||||
* @throws IOException if the socket could not be made ready
|
||||
*/
|
||||
private void checkConnection() throws IOException {
|
||||
synchronized (socketLock) {
|
||||
try {
|
||||
// Longer sink time used for initial connection welcome > prompt
|
||||
final int sinkTime;
|
||||
if (isConnected()) {
|
||||
sinkTime = SINK_TIMEOUT_MS;
|
||||
} else {
|
||||
sinkTime = SOCKET_TIMEOUT_MS;
|
||||
connect();
|
||||
}
|
||||
|
||||
final Socket socket = this.socket;
|
||||
if (socket == null) {
|
||||
throw new IllegalStateException(
|
||||
"Socket is null, which is unexpected because it was verified as available earlier in the same synchronized block; please log a bug report");
|
||||
}
|
||||
final InputStream in = socket.getInputStream();
|
||||
|
||||
// Sink (clear) buffer until earlier of the sinkTime or > prompt
|
||||
try {
|
||||
socket.setSoTimeout(sinkTime);
|
||||
logger.trace("Waiting {} ms for buffer to sink", sinkTime);
|
||||
while (true) {
|
||||
int b = in.read();
|
||||
if (b == -1) {
|
||||
break;
|
||||
}
|
||||
if (b == PROMPT) {
|
||||
if (in.available() > 0) {
|
||||
throw new IOException("Unexpected data following prompt");
|
||||
}
|
||||
logger.trace("Buffer empty following unsolicited > prompt");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (final SocketTimeoutException expectedFromRead) {
|
||||
} finally {
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
// Solicit for a prompt given we haven't received one earlier
|
||||
final Writer out = new OutputStreamWriter(socket.getOutputStream(), US_ASCII);
|
||||
out.write(LF);
|
||||
out.flush();
|
||||
|
||||
// Block until the > prompt arrives or IOE if SOCKET_TIMEOUT_MS
|
||||
final int b = in.read();
|
||||
if (b != PROMPT) {
|
||||
throw new IOException("Unexpected character received");
|
||||
}
|
||||
if (in.available() > 0) {
|
||||
throw new IOException("Unexpected data following prompt");
|
||||
}
|
||||
logger.trace("Buffer empty following solicited > prompt");
|
||||
} catch (final IOException ioe) {
|
||||
disconnect();
|
||||
throw ioe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the socket.
|
||||
*
|
||||
* <p>
|
||||
* Guarantees to either open the socket or thrown an exception.
|
||||
*
|
||||
* @throws IOException if the socket could not be opened
|
||||
*/
|
||||
private void connect() throws IOException {
|
||||
synchronized (socketLock) {
|
||||
try {
|
||||
logger.debug("Connecting to {}:{}", cfg.host, cfg.port);
|
||||
final Socket socket = new Socket();
|
||||
socket.connect(new InetSocketAddress(cfg.host, cfg.port), SOCKET_TIMEOUT_MS);
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
|
||||
this.socket = socket;
|
||||
} catch (final IOException ioe) {
|
||||
socket = null;
|
||||
throw ioe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to disconnect the socket.
|
||||
*
|
||||
* <p>
|
||||
* Disconnection failure is not considered an error, although will be logged.
|
||||
*/
|
||||
private void disconnect() {
|
||||
synchronized (socketLock) {
|
||||
final Socket socket = this.socket;
|
||||
if (socket != null) {
|
||||
logger.debug("Disconnecting from {}:{}", cfg.host, cfg.port);
|
||||
try {
|
||||
socket.close();
|
||||
} catch (final IOException ioe) {
|
||||
logger.warn("Could not disconnect", ioe);
|
||||
}
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes ASCII I/F protocol error messages.
|
||||
*
|
||||
* <p>
|
||||
* This exception is not used for normal socket and connection failures.
|
||||
* It is only used when there is a protocol level error (eg unexpected
|
||||
* messages or malformed content from the CoolNetMaster server).
|
||||
*/
|
||||
public class CoolMasterClientError extends IOException {
|
||||
private static final long serialVersionUID = 2L;
|
||||
|
||||
public CoolMasterClientError(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CoolMasterClientError(String format, Object... args) {
|
||||
super(String.format(format, args));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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.coolmasternet.internal;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* Defines constants used across the whole binding.
|
||||
*
|
||||
* @author Angus Gratton - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CoolMasterNetBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "coolmasternet";
|
||||
|
||||
// list of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller");
|
||||
public static final ThingTypeUID THING_TYPE_HVAC = new ThingTypeUID(BINDING_ID, "hvac");
|
||||
|
||||
// list of all Channel ids
|
||||
public static final String ON = "on";
|
||||
public static final String MODE = "mode";
|
||||
public static final String SET_TEMP = "set_temp";
|
||||
public static final String FAN_SPEED = "fan_speed";
|
||||
public static final String LOUVRE = "louvre";
|
||||
public static final String CURRENT_TEMP = "current_temp";
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream.of(THING_TYPE_CONTROLLER, THING_TYPE_HVAC)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.coolmasternet.internal;
|
||||
|
||||
import static org.openhab.binding.coolmasternet.internal.CoolMasterNetBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.coolmasternet.internal.handler.HVACHandler;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link CoolMasterNetHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Angus Gratton - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.coolmasternet")
|
||||
public class CoolMasterNetHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(final Thing thing) {
|
||||
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_CONTROLLER)) {
|
||||
return new ControllerHandler((Bridge) thing);
|
||||
} else if (thingTypeUID.equals(THING_TYPE_HVAC)) {
|
||||
return new HVACHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.coolmasternet.internal.config;
|
||||
|
||||
/**
|
||||
* The {@link ControllerConfiguration} is responsible for holding configuration information needed to access/poll the
|
||||
* CoolMasterNet Controller.
|
||||
*
|
||||
* @author Angus Gratton - Initial contribution
|
||||
* @author Wouter Born - Split Controller and HVAC configurations
|
||||
*/
|
||||
public class ControllerConfiguration {
|
||||
|
||||
public static final String HOST = "host";
|
||||
public static final String PORT = "port";
|
||||
public static final String REFRESH = "refresh";
|
||||
|
||||
public String host;
|
||||
public int port = 10102;
|
||||
public int refresh = 5; // seconds
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.coolmasternet.internal.config;
|
||||
|
||||
/**
|
||||
* The {@link HVACConfiguration} is responsible for holding configuration information needed to access/poll the
|
||||
* HVAC unit.
|
||||
*
|
||||
* @author Angus Gratton - Initial contribution
|
||||
* @author Wouter Born - Split Controller and HVAC configurations
|
||||
*/
|
||||
public class HVACConfiguration {
|
||||
|
||||
public static final String UID = "uid";
|
||||
public String uid;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 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.coolmasternet.internal.handler;
|
||||
|
||||
import static org.openhab.binding.coolmasternet.internal.CoolMasterNetBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.coolmasternet.internal.ControllerHandler;
|
||||
import org.openhab.binding.coolmasternet.internal.ControllerHandler.CoolMasterClientError;
|
||||
import org.openhab.binding.coolmasternet.internal.config.HVACConfiguration;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link HVACHandler} is responsible for handling commands for a single
|
||||
* HVAC unit (a single UID on a CoolMasterNet controller.)
|
||||
*
|
||||
* @author Angus Gratton - Initial contribution
|
||||
* @author Wouter Born - Fix null pointer exceptions
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HVACHandler extends BaseThingHandler {
|
||||
|
||||
/**
|
||||
* The CoolMasterNet protocol's query command returns numbers 0-5 for fan
|
||||
* speed, but the protocol's fan command (and matching binding command) use
|
||||
* single-letter abbreviations.
|
||||
*/
|
||||
private static final Map<String, @Nullable String> FAN_NUM_TO_STR;
|
||||
|
||||
/**
|
||||
* The CoolMasterNet query command returns numbers 0-5 for operation modes,
|
||||
* but these don't map to any mode you can set on the device, so we use this
|
||||
* lookup table.
|
||||
*/
|
||||
private static final Map<String, @Nullable String> MODE_NUM_TO_STR;
|
||||
|
||||
static {
|
||||
FAN_NUM_TO_STR = new HashMap<>();
|
||||
FAN_NUM_TO_STR.put("0", "l"); // low
|
||||
FAN_NUM_TO_STR.put("1", "m"); // medium
|
||||
FAN_NUM_TO_STR.put("2", "h"); // high
|
||||
FAN_NUM_TO_STR.put("3", "a"); // auto
|
||||
FAN_NUM_TO_STR.put("4", "t"); // top
|
||||
|
||||
MODE_NUM_TO_STR = new HashMap<>();
|
||||
MODE_NUM_TO_STR.put("0", "cool");
|
||||
MODE_NUM_TO_STR.put("1", "heat");
|
||||
MODE_NUM_TO_STR.put("2", "auto");
|
||||
MODE_NUM_TO_STR.put("3", "dry");
|
||||
// 4=='haux' but this mode doesn't have an equivalent command to set it
|
||||
MODE_NUM_TO_STR.put("4", "heat");
|
||||
MODE_NUM_TO_STR.put("5", "fan");
|
||||
}
|
||||
|
||||
private HVACConfiguration cfg = new HVACConfiguration();
|
||||
private final Logger logger = LoggerFactory.getLogger(HVACHandler.class);
|
||||
|
||||
public HVACHandler(final Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the controller handler for this bridge.
|
||||
*
|
||||
* <p>
|
||||
* This method does not raise any exception, but if null is returned it will
|
||||
* always update the Thing status with the reason.
|
||||
*
|
||||
* <p>
|
||||
* The returned handler may or may not be connected. This method will not
|
||||
* change the Thing status simply because it is not connected, because a
|
||||
* caller may wish to attempt an operation that would result in connection.
|
||||
*
|
||||
* @return the controller handler or null if the controller is unavailable
|
||||
*/
|
||||
private @Nullable ControllerHandler getControllerHandler() {
|
||||
final Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"CoolMasterNet Controller bridge not configured");
|
||||
return null;
|
||||
}
|
||||
|
||||
final ControllerHandler handler = (ControllerHandler) bridge.getHandler();
|
||||
|
||||
if (handler == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
|
||||
"CoolMasterNet Controller bridge not initialized");
|
||||
return null;
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
final ControllerHandler controller = getControllerHandler();
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String uid = cfg.uid;
|
||||
final String channel = channelUID.getId();
|
||||
|
||||
try {
|
||||
switch (channel) {
|
||||
case CURRENT_TEMP:
|
||||
if (command instanceof RefreshType) {
|
||||
final String currentTemp = query(controller, "a");
|
||||
if (currentTemp != null) {
|
||||
final Integer temp = new Integer(currentTemp);
|
||||
final QuantityType<?> value = new QuantityType<>(temp, controller.getUnit());
|
||||
updateState(CURRENT_TEMP, value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ON:
|
||||
if (command instanceof RefreshType) {
|
||||
final String on = query(controller, "o");
|
||||
if (on != null) {
|
||||
updateState(ON, "1".equals(on) ? OnOffType.ON : OnOffType.OFF);
|
||||
}
|
||||
} else if (command instanceof OnOffType) {
|
||||
final OnOffType onoff = (OnOffType) command;
|
||||
controller.sendCommand(String.format("%s %s", onoff == OnOffType.ON ? "on" : "off", uid));
|
||||
}
|
||||
break;
|
||||
case SET_TEMP:
|
||||
if (command instanceof RefreshType) {
|
||||
final String setTemp = query(controller, "t");
|
||||
if (setTemp != null) {
|
||||
final Integer temp = new Integer(setTemp);
|
||||
final QuantityType<?> value = new QuantityType<>(temp, controller.getUnit());
|
||||
updateState(SET_TEMP, value);
|
||||
}
|
||||
} else if (command instanceof QuantityType) {
|
||||
final QuantityType<?> temp = (QuantityType) command;
|
||||
final QuantityType<?> converted = temp.toUnit(controller.getUnit());
|
||||
final String formatted = converted.format("%.1f");
|
||||
controller.sendCommand(String.format("temp %s %s", uid, formatted));
|
||||
}
|
||||
break;
|
||||
case MODE:
|
||||
if (command instanceof RefreshType) {
|
||||
final String mode = MODE_NUM_TO_STR.get(query(controller, "m"));
|
||||
if (mode != null) {
|
||||
updateState(MODE, new StringType(mode));
|
||||
}
|
||||
} else if (command instanceof StringType) {
|
||||
final String mode = ((StringType) command).toString();
|
||||
controller.sendCommand(String.format("%s %s", mode, uid));
|
||||
}
|
||||
break;
|
||||
case FAN_SPEED:
|
||||
if (command instanceof RefreshType) {
|
||||
final String fan = FAN_NUM_TO_STR.get(query(controller, "f"));
|
||||
if (fan != null) {
|
||||
updateState(FAN_SPEED, new StringType(fan));
|
||||
}
|
||||
} else if (command instanceof StringType) {
|
||||
final String fan = ((StringType) command).toString();
|
||||
controller.sendCommand(String.format("fspeed %s %s", uid, fan));
|
||||
}
|
||||
break;
|
||||
case LOUVRE:
|
||||
if (command instanceof RefreshType) {
|
||||
final String louvre = query(controller, "s");
|
||||
if (louvre != null) {
|
||||
updateState(LOUVRE, new StringType(louvre));
|
||||
}
|
||||
} else if (command instanceof StringType) {
|
||||
final String louvre = ((StringType) command).toString();
|
||||
controller.sendCommand(String.format("swing %s %s", uid, louvre));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.warn("Unknown command '{}' on channel '{}' for unit '{}'", command, channel, uid);
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (final IOException ioe) {
|
||||
logger.warn("Failed to handle command '{}' on channel '{}' for unit '{}' due to '{}'", command, channel,
|
||||
uid, ioe.getLocalizedMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ioe.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
cfg = getConfigAs(HVACConfiguration.class);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this HVAC unit's properties from the controller.
|
||||
*/
|
||||
public void refresh() {
|
||||
for (final Channel channel : getThing().getChannels()) {
|
||||
handleCommand(channel.getUID(), RefreshType.REFRESH);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String query(final ControllerHandler controller, final String queryChar)
|
||||
throws IOException, CoolMasterClientError {
|
||||
final String uid = getConfigAs(HVACConfiguration.class).uid;
|
||||
final String command = String.format("query %s %s", uid, queryChar);
|
||||
return controller.sendCommand(command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="coolmasternet" 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>CoolMasterNet Binding</name>
|
||||
<description>Binding for Cool Automation CoolMasterNet HVAC controllers.</description>
|
||||
<author>Two Feathers Pty Ltd</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="coolmasternet"
|
||||
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">
|
||||
|
||||
<bridge-type id="controller">
|
||||
<label>CoolMasterNet Controller</label>
|
||||
<description>A CoolMasterNet Controller (connected to one or more HVAC systems)</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<label>Hostname</label>
|
||||
<context>network-address</context>
|
||||
<description>The IP address / FQDN of the CoolMasterNet unit</description>
|
||||
<default></default>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
|
||||
<parameter name="port" type="integer">
|
||||
<label>Port</label>
|
||||
<description>Port of ASCII interface of CoolMasterNet unit.</description>
|
||||
<default>10102</default>
|
||||
<required>false</required>
|
||||
</parameter>
|
||||
|
||||
<parameter name="refresh" type="integer">
|
||||
<label>Refresh Frequency</label>
|
||||
<description>Frequency to poll the controller for updates, in seconds. Defaults to every 5 seconds.</description>
|
||||
<default>5</default>
|
||||
<required>false</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="hvac">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="controller"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>HVAC System</label>
|
||||
<description>HVAC System connected to a controller (unique UID)</description>
|
||||
|
||||
<channels>
|
||||
<channel id="on" typeId="power"/>
|
||||
<channel id="mode" typeId="hvac_mode"/>
|
||||
<channel id="fan_speed" typeId="fan_speed"/>
|
||||
<channel id="set_temp" typeId="temperature_setpoint"/>
|
||||
<channel id="current_temp" typeId="temperature_readback"/>
|
||||
<channel id="louvre" typeId="louvre_angle"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="uid" type="text">
|
||||
<label>HVAC Unit ID</label>
|
||||
<description>Unit ID of the HVAC Unit to control. Example: L1.100.</description>
|
||||
<default>L1.100</default>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="power">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Power</label>
|
||||
<description>Is the HVAC unit powered on?</description>
|
||||
<category>Switch</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="hvac_mode">
|
||||
<item-type>String</item-type>
|
||||
<label>Mode</label>
|
||||
<description>HVAC unit operation mode</description>
|
||||
<state>
|
||||
<options>
|
||||
<!-- Note: The value fields here map directly to coolmasternet commands -->
|
||||
<option value="cool">Cool</option>
|
||||
<option value="heat">Heat</option>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="dry">Dry</option>
|
||||
<option value="fan">Fan Only</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="temperature_setpoint" advanced="true">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Set Temperature</label>
|
||||
<description>Temperature thermostat setpoint</description>
|
||||
<category>Temperature</category>
|
||||
<state pattern="%d %unit%" min="10" max="40" step="1"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="temperature_readback" advanced="true">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Current Temperature</label>
|
||||
<description>Current system ambient temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state pattern="%d %unit%" min="10" max="40" step="1" readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="fan_speed">
|
||||
<item-type>String</item-type>
|
||||
<label>Fan Speed</label>
|
||||
<state>
|
||||
<options>
|
||||
<option value="l">Low</option>
|
||||
<option value="m">Medium</option>
|
||||
<option value="h">High</option>
|
||||
<option value="t">Top</option>
|
||||
<option value="a">Auto</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="louvre_angle">
|
||||
<item-type>String</item-type>
|
||||
<label>Louvre Position</label>
|
||||
<state>
|
||||
<options>
|
||||
<option value="0">No Control</option>
|
||||
<option value="a">Auto Swing</option>
|
||||
<option value="h">Horizontal</option>
|
||||
<option value="3">30 degrees</option>
|
||||
<option value="4">45 degrees</option>
|
||||
<option value="6">60 degrees</option>
|
||||
<option value="v">Vertical</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user