diff --git a/bundles/org.openhab.binding.intesis/README.md b/bundles/org.openhab.binding.intesis/README.md index b77b6e9b78951..b3567c0f9fd79 100644 --- a/bundles/org.openhab.binding.intesis/README.md +++ b/bundles/org.openhab.binding.intesis/README.md @@ -1,7 +1,6 @@ # Intesis Binding -This binding connects to WiFi [IntesisHome](http://www.intesishome.com/) devices using their local REST Api. -It does actually not support [IntesisBox](http://www.intesisbox.com/) devices but support is planned in upcoming version. +This binding connects to WiFi [IntesisHome](https://www.intesis.com/products/cloud-solutions/ac-cloud-control) devices using their local REST Api and to [IntesisBox](https://www.intesis.com/products/ac-interfaces/wifi-gateways) devices using TCP connection. @@ -9,9 +8,10 @@ It does actually not support [IntesisBox](http://www.intesisbox.com/) devices bu This binding only supports one thing type: -| Thing | Thing Type | Description | -|------------ |------------|---------------------------------| -| intesisHome | Thing | Represents a single WiFi device | +| Thing | Thing Type | Description | +|-------------|------------|---------------------------------------------| +| intesisHome | Thing | Represents a single IntesisHome WiFi device | +| intesisBox | Thing | Represents a single IntesisBox WiFi device | ## Discovery @@ -19,35 +19,40 @@ Intesis devices do not support auto discovery. ## Thing Configuration -The binding needs two configuration parameters. +The binding uses the following configuration parameters. -| Parameter | Description | -|-----------|---------------------------------------------------| -| ipAddress | IP-Address of the device | -| password | Password to login to the local webserver of device | +| Parameter | Valid for ThingType | Description | +|-----------|---------------------|----------------------------------------------------------------| +| ipAddress | Both | IP-Address of the device | +| password | IntesisHome | Password to login to the local webserver of IntesisHome device | +| port | IntesisBox | TCP port to connect to IntesisBox device, defaults to 3310 | ## Channels -| Channel ID | Item Type | Description | Possible Values | -|--------------------|--------------------|---------------------------------------------|---------------------------| -| power | Switch | Turns power on/off for your climate system. | ON, OFF | -| mode | String | The heating/cooling mode. | AUTO,HEAT,DRY,FAN,COOL | -| fanSpeed | String | Fan speed (if applicable) | AUTO,1-10 | -| vanesUpDown | String | Control of up/down vanes (if applicable) | AUTO,1-9,SWING,SWIRL,WIDE | -| vanesUpDown | String | Control of left/right vanes (if applicable) | AUTO,1-9,SWING,SWIRL,WIDE | -| targetTemperature | Number:Temperature | The currently set target temperature. | | -| ambientTemperature | Number:Temperature | (Readonly) The ambient air temperature. | | -| outdoorTemperature | Number:Temperature | (Readonly) The outdoor air temperature. | | +| Channel ID | Item Type | Description | Possible Values | +|--------------------|--------------------|--------------------------------------------------------|---------------------------------------------------------| +| power | Switch | Turns power on/off for your climate system. | ON,OFF | +| mode | String | The heating/cooling mode. | AUTO,HEAT,DRY,FAN,COOL | +| fanSpeed | String | Fan speed (if applicable) | AUTO,1-10 | +| vanesUpDown | String | Control of up/down vanes (if applicable) | AUTO,1-9,SWING,SWIRL,WIDE | +| vanesUpDown | String | Control of left/right vanes (if applicable) | AUTO,1-9,SWING,SWIRL,WIDE | +| targetTemperature | Number:Temperature | The currently set target temperature (if applicable) | range between 18°C and 30°C | +| ambientTemperature | Number:Temperature | (Readonly) The ambient air temperature (if applicable) | | +| outdoorTemperature | Number:Temperature | (Readonly) The outdoor air temperature (if applicable) | | +| errorStatus | String | (Readonly) The error status of the device | OK,ERR | +| errorCode | String | (Readonly) The error code if an error encountered | not documented | +| wifiSignal | Number | (Readonly) WiFi signal strength (IntesisBox only) | 4=excellent, 3=good, 2=not string, 1=unreliable, 0=none | Note that individual A/C units may not support all channels, or all possible values for those channels. The binding will add all supported channels and possible values on first thing initialization and list them as thing properties. -If new channels or values might be supported after firmware upgrades, deleting the thing and reading is necessary. -For example, not all A/C units have controllable vanes. Or fan speed may be limited to 1-4, instead of all of 1-9. -The set point temperature is also limited to a device specific range. For set point temperature, sending an invalid value +If new channels or values might be supported after firmware upgrades, deleting the thing and re-adding is necessary. +For example, not all A/C units have controllable vanes or fan speed may be limited to 1-4, instead of all of 1-9. +The target temperature is also limited to a device specific range. For target temperature, sending an invalid value will cause it to choose the minimum/maximum allowable value as appropriate. The device will also round it to whatever step size it supports. For all other channels, invalid values are ignored. +IntesisBox firmware 1.3.3 reports temperatures by full degrees only (e.g. 23.0) even if a half degree (e.g. 23.5) was set. ## Full Example @@ -55,8 +60,9 @@ The binding can be fully setup from the UI but if you decide to use files here i **Things** -```intesisHome.things +``` Thing intesis:intesisHome:acOffice "AC Unit Adapter" @ "AC" [ipAddress="192.168.1.100", password="xxxxx"] +Thing intesis:intesisBox:acOffice "AC Unit Adapter" @ "AC" [ipAddress="192.168.1.100", port=3310] ``` **Items** @@ -89,4 +95,3 @@ sitemap intesishome label="My AC control" { } } ``` - diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisBindingConstants.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisBindingConstants.java index e90a8178db56e..4503e96d728aa 100644 --- a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisBindingConstants.java +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisBindingConstants.java @@ -31,6 +31,7 @@ public class IntesisBindingConstants { // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_INTESISHOME = new ThingTypeUID(BINDING_ID, "intesisHome"); + public static final ThingTypeUID THING_TYPE_INTESISBOX = new ThingTypeUID(BINDING_ID, "intesisBox"); // List of all Channel ids public static final String CHANNEL_TYPE_POWER = "power"; @@ -41,4 +42,7 @@ public class IntesisBindingConstants { public static final String CHANNEL_TYPE_TARGETTEMP = "targetTemperature"; public static final String CHANNEL_TYPE_AMBIENTTEMP = "ambientTemperature"; public static final String CHANNEL_TYPE_OUTDOORTEMP = "outdoorTemperature"; + public static final String CHANNEL_TYPE_ERRORCODE = "errorCode"; + public static final String CHANNEL_TYPE_ERRORSTATUS = "errorStatus"; + public static final String CHANNEL_TYPE_RSSI = "wifiSignal"; } diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisHandlerFactory.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisHandlerFactory.java index 1bee55fad4ff0..fd48f81372c2d 100644 --- a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisHandlerFactory.java +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisHandlerFactory.java @@ -12,14 +12,17 @@ */ package org.openhab.binding.intesis.internal; -import static org.openhab.binding.intesis.internal.IntesisBindingConstants.THING_TYPE_INTESISHOME; +import static org.openhab.binding.intesis.internal.IntesisBindingConstants.*; import java.util.Collections; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.intesis.internal.handler.IntesisBoxHandler; import org.openhab.binding.intesis.internal.handler.IntesisHomeHandler; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Thing; @@ -48,7 +51,8 @@ public class IntesisHandlerFactory extends BaseThingHandlerFactory { private final HttpClient httpClient; private final IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider; - private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_INTESISHOME); + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_INTESISHOME, THING_TYPE_INTESISBOX).collect(Collectors.toSet())); @Activate public IntesisHandlerFactory(@Reference HttpClientFactory httpClientFactory, ComponentContext componentContext, @@ -69,10 +73,13 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_INTESISHOME.equals(thingTypeUID)) { - logger.debug("Creating a IntesisHomeHandler for thing '{}'", thing.getUID()); return new IntesisHomeHandler(thing, httpClient, intesisStateDescriptionProvider); } + if (THING_TYPE_INTESISBOX.equals(thingTypeUID)) { + return new IntesisBoxHandler(thing, intesisStateDescriptionProvider); + } + return null; } } diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxChangeListener.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxChangeListener.java new file mode 100644 index 0000000000000..a61ad69c21625 --- /dev/null +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxChangeListener.java @@ -0,0 +1,37 @@ +/** + * 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.intesis.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.ThingStatus; + +/** + * The {@link IntesisBoxChangeListener} is in interface for a IntesisBox changed consumer + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public interface IntesisBoxChangeListener { + /** + * This method will be called in case a message was received. + * + */ + void messageReceived(String messageLine); + + /** + * This method will be called in case the connection status has changed. + * + */ + void connectionStatusChanged(ThingStatus status, @Nullable String message); +} diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxMessage.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxMessage.java new file mode 100644 index 0000000000000..cfe26429e18cc --- /dev/null +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxMessage.java @@ -0,0 +1,78 @@ +/** + * 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.intesis.internal.api; + +import java.util.Arrays; +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; + +/** + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class IntesisBoxMessage { + public static final String ID = "ID"; + public static final String INFO = "INFO"; + public static final String SET = "SET"; + public static final String CHN = "CHN"; + public static final String GET = "GET"; + public static final String LOGIN = "LOGIN"; + public static final String LOGOUT = "LOGOUT"; + public static final String CFG = "CFG"; + public static final String LIMITS = "LIMITS"; + public static final String DISCOVER = "DISCOVER"; + + private static final Pattern REGEX = Pattern.compile("^([^,]+)(?:,(\\d+))?:([^,]+),([A-Z0-9.,\\[\\]]+)$"); + + @SuppressWarnings("unused") + private final String acNum; + private final String command; + private final String function; + private final String value; + + private IntesisBoxMessage(String command, String acNum, String function, String value) { + this.command = command; + this.acNum = acNum; + this.function = function; + this.value = value; + } + + public String getCommand() { + return command; + } + + public String getFunction() { + return function; + } + + public String getValue() { + return value; + } + + public List getLimitsValue() { + return Arrays.asList(value.substring(1, value.length() - 1).split(",")); + } + + public static @Nullable IntesisBoxMessage parse(String message) { + Matcher m = REGEX.matcher(message); + if (!m.find()) { + return null; + } + + return new IntesisBoxMessage(m.group(1), m.group(2), m.group(3), m.group(4)); + } +} diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxSocketApi.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxSocketApi.java new file mode 100644 index 0000000000000..c087532abce67 --- /dev/null +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisBoxSocketApi.java @@ -0,0 +1,214 @@ +/** + * 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.intesis.internal.api; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.intesis.internal.handler.IntesisBoxHandler; +import org.openhab.core.thing.ThingStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class handling the Socket connections. + * + * @author Cody Cutrer - Initial contribution + * @author Hans-Jörg Merk - Moved Socket to it's own class + */ +@NonNullByDefault +public class IntesisBoxSocketApi { + + private final Logger logger = LoggerFactory.getLogger(IntesisBoxSocketApi.class); + + private final String ipAddress; + private final int port; + private final String readerThreadName; + + private @Nullable IntesisSocket tcpSocket = null; + private @Nullable OutputStreamWriter tcpOutput = null; + private @Nullable BufferedReader tcpInput = null; + + private @Nullable IntesisBoxChangeListener changeListener; + + private boolean connected = false; + + public IntesisBoxSocketApi(final String ipAddress, final int port, final String readerThreadName) { + this.ipAddress = ipAddress; + this.port = port; + this.readerThreadName = readerThreadName; + } + + private class IntesisSocket { + final Socket socket; + + public IntesisSocket() throws UnknownHostException, IOException { + socket = new Socket(); + SocketAddress tcpSocketAddress = new InetSocketAddress(ipAddress, port); + socket.connect(tcpSocketAddress); + } + + public void close() throws IOException { + socket.close(); + } + } + + public void openConnection() throws IOException { + closeConnection(); + + IntesisBoxChangeListener listener = this.changeListener; + IntesisSocket localSocket = tcpSocket = new IntesisSocket(); + tcpOutput = new OutputStreamWriter(localSocket.socket.getOutputStream(), StandardCharsets.US_ASCII); + tcpInput = new BufferedReader( + new InputStreamReader(localSocket.socket.getInputStream(), StandardCharsets.US_ASCII)); + + Thread tcpListener = new Thread(new TCPListener()); + tcpListener.setName(readerThreadName); + tcpListener.setDaemon(true); + tcpListener.start(); + + setConnected(true); + if (listener != null) { + listener.connectionStatusChanged(ThingStatus.ONLINE, null); + } + } + + public void closeConnection() { + try { + IntesisSocket localSocket = tcpSocket; + OutputStreamWriter localOutput = tcpOutput; + BufferedReader localInput = tcpInput; + + if (localSocket != null) { + localSocket.close(); + localSocket = null; + } + if (localInput != null) { + localInput.close(); + localInput = null; + } + if (localOutput != null) { + localOutput.close(); + localOutput = null; + } + setConnected(false); + } catch (IOException ioException) { + logger.debug("closeConnection(): Unable to close connection - {}", ioException.getMessage()); + } catch (Exception exception) { + logger.debug("closeConnection(): Error closing connection - {}", exception.getMessage()); + } + } + + private class TCPListener implements Runnable { + + /** + * Run method. Runs the MessageListener thread + */ + @Override + public void run() { + while (isConnected()) { + String message = read(); + readMessage(message); + } + } + } + + public void addIntesisBoxChangeListener(IntesisBoxChangeListener listener) { + if (this.changeListener == null) { + this.changeListener = listener; + } + } + + private void write(String data) { + IntesisBoxChangeListener listener = this.changeListener; + try { + OutputStreamWriter localOutput = tcpOutput; + + if (localOutput != null) { + localOutput.write(data); + localOutput.flush(); + } + } catch (IOException ioException) { + setConnected(false); + if (listener != null) { + listener.connectionStatusChanged(ThingStatus.OFFLINE, ioException.getMessage()); + } + } + } + + public String read() { + String message = ""; + try { + BufferedReader localInput = tcpInput; + if (localInput != null) { + message = localInput.readLine(); + } + } catch (IOException ioException) { + setConnected(false); + } + return message; + } + + public void readMessage(String message) { + IntesisBoxChangeListener listener = this.changeListener; + + if (listener != null && !message.isEmpty()) { + listener.messageReceived(message); + } + } + + public void sendAlive() { + write("GET,1:*\r\n"); + } + + public void sendId() { + write("ID\r\n"); + } + + public void sendLimitsQuery() { + write("LIMITS:*\r\n"); + } + + public void sendCommand(String function, String value) { + String data = String.format("SET,1:%s,%s\r\n", function, value); + write(data); + } + + public void sendQuery(String function) { + String data = String.format("GET,1:%s\r\n", function); + write(data); + } + + public boolean isConnected() { + return this.connected; + } + + public void setConnected(boolean connected) { + this.connected = connected; + } + + public void removeIntesisBoxChangeListener(IntesisBoxHandler intesisBoxHandler) { + if (this.changeListener != null) { + this.changeListener = null; + } + } +} diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisHomeHttpApi.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisHomeHttpApi.java index 7bbeb095b1864..5880234a3a1ec 100644 --- a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisHomeHttpApi.java +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/IntesisHomeHttpApi.java @@ -23,7 +23,7 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; -import org.openhab.binding.intesis.internal.IntesisConfiguration; +import org.openhab.binding.intesis.internal.config.IntesisHomeConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +40,7 @@ public class IntesisHomeHttpApi { private final Logger logger = LoggerFactory.getLogger(IntesisHomeHttpApi.class); private final HttpClient httpClient; - public IntesisHomeHttpApi(IntesisConfiguration config, HttpClient httpClient) { + public IntesisHomeHttpApi(IntesisHomeConfiguration config, HttpClient httpClient) { this.httpClient = httpClient; } diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/MessageReceivedEvent.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/MessageReceivedEvent.java new file mode 100644 index 0000000000000..aea77479b6f76 --- /dev/null +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/api/MessageReceivedEvent.java @@ -0,0 +1,39 @@ +/** + * 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.intesis.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link DataPointChangedEvent} is an event container for data point changes + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class MessageReceivedEvent { + protected String message; + + public MessageReceivedEvent(Object source, String message) { + this.message = message; + } + + /** + * Gets the data-point of the event. + * + */ + @Nullable + public String getMessage() { + return this.message; + } +} diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/config/IntesisBoxConfiguration.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/config/IntesisBoxConfiguration.java new file mode 100644 index 0000000000000..21ec814e0303d --- /dev/null +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/config/IntesisBoxConfiguration.java @@ -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.intesis.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link IntesisBoxConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class IntesisBoxConfiguration { + public String ipAddress = ""; + public int port; +} diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisConfiguration.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/config/IntesisHomeConfiguration.java similarity index 74% rename from bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisConfiguration.java rename to bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/config/IntesisHomeConfiguration.java index 31860257a4404..58fa954720c23 100644 --- a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisConfiguration.java +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/config/IntesisHomeConfiguration.java @@ -10,17 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.intesis.internal; +package org.openhab.binding.intesis.internal.config; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link IntesisConfiguration} class contains fields mapping thing configuration parameters. + * The {@link IntesisHomeConfiguration} class contains fields mapping thing configuration parameters. * * @author Hans-Jörg Merk - Initial contribution */ @NonNullByDefault -public class IntesisConfiguration { +public class IntesisHomeConfiguration { public String ipAddress = ""; public String password = ""; } diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisHomeModeEnum.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/enums/IntesisHomeModeEnum.java similarity index 94% rename from bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisHomeModeEnum.java rename to bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/enums/IntesisHomeModeEnum.java index c63ddb65df9b5..7aae91cecdaa9 100644 --- a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/IntesisHomeModeEnum.java +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/enums/IntesisHomeModeEnum.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.intesis.internal; +package org.openhab.binding.intesis.internal.enums; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisBoxHandler.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisBoxHandler.java new file mode 100644 index 0000000000000..ee56499b07716 --- /dev/null +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisBoxHandler.java @@ -0,0 +1,394 @@ +/** + * 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.intesis.internal.handler; + +import static org.openhab.binding.intesis.internal.IntesisBindingConstants.*; +import static org.openhab.binding.intesis.internal.api.IntesisBoxMessage.*; +import static org.openhab.core.thing.Thing.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.intesis.internal.IntesisDynamicStateDescriptionProvider; +import org.openhab.binding.intesis.internal.api.IntesisBoxChangeListener; +import org.openhab.binding.intesis.internal.api.IntesisBoxMessage; +import org.openhab.binding.intesis.internal.api.IntesisBoxSocketApi; +import org.openhab.binding.intesis.internal.config.IntesisBoxConfiguration; +import org.openhab.core.library.types.DecimalType; +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.library.unit.SIUnits; +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.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link IntesisBoxHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Cody Cutrer - Initial contribution + * @author Rocky Amatulli - additions to include id message handling, dynamic channel options based on limits. + * @author Hans-Jörg Merk - refactored for openHAB 3.0 compatibility + * + */ +@NonNullByDefault +public class IntesisBoxHandler extends BaseThingHandler implements IntesisBoxChangeListener { + + private final Logger logger = LoggerFactory.getLogger(IntesisBoxHandler.class); + private @Nullable IntesisBoxSocketApi intesisBoxSocketApi; + + private final Map properties = new HashMap<>(); + private final Map> limits = new HashMap<>(); + + private final IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider; + + private IntesisBoxConfiguration config = new IntesisBoxConfiguration(); + + private double minTemp = 0.0, maxTemp = 0.0; + + private boolean hasProperties = false; + + private @Nullable ScheduledFuture pollingTask; + + public IntesisBoxHandler(Thing thing, IntesisDynamicStateDescriptionProvider intesisStateDescriptionProvider) { + super(thing); + this.intesisStateDescriptionProvider = intesisStateDescriptionProvider; + } + + @Override + public void initialize() { + config = getConfigAs(IntesisBoxConfiguration.class); + + if (!config.ipAddress.isEmpty()) { + + updateStatus(ThingStatus.UNKNOWN); + scheduler.submit(() -> { + + String readerThreadName = "OH-binding-" + getThing().getUID().getAsString(); + + IntesisBoxSocketApi intesisLocalApi = intesisBoxSocketApi = new IntesisBoxSocketApi(config.ipAddress, + config.port, readerThreadName); + intesisLocalApi.addIntesisBoxChangeListener(this); + try { + intesisLocalApi.openConnection(); + intesisLocalApi.sendId(); + intesisLocalApi.sendLimitsQuery(); + intesisLocalApi.sendAlive(); + + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + return; + } + updateStatus(ThingStatus.ONLINE); + }); + pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 3, 45, TimeUnit.SECONDS); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address specified)"); + } + } + + @Override + public void dispose() { + final ScheduledFuture pollingTask = this.pollingTask; + + IntesisBoxSocketApi api = this.intesisBoxSocketApi; + + if (pollingTask != null) { + pollingTask.cancel(true); + this.pollingTask = null; + } + if (api != null) { + api.closeConnection(); + api.removeIntesisBoxChangeListener(this); + } + super.dispose(); + } + + private synchronized void polling() { + IntesisBoxSocketApi api = this.intesisBoxSocketApi; + if (api != null) { + if (!api.isConnected()) { + try { + api.openConnection(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + api.sendAlive(); + api.sendId(); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + IntesisBoxSocketApi api = this.intesisBoxSocketApi; + if (api != null) { + if (!api.isConnected()) { + logger.trace("Sending command failed, not connected"); + return; + } + if (command instanceof RefreshType) { + logger.trace("Refresh channel {}", channelUID.getId()); + api.sendQuery(channelUID.getId()); + return; + } + } + String value = ""; + String function = ""; + switch (channelUID.getId()) { + case CHANNEL_TYPE_POWER: + if (command instanceof OnOffType) { + function = "ONOFF"; + value = command == OnOffType.ON ? "ON" : "OFF"; + } + break; + case CHANNEL_TYPE_TARGETTEMP: + if (command instanceof QuantityType) { + QuantityType celsiusTemperature = (QuantityType) command; + celsiusTemperature = celsiusTemperature.toUnit(SIUnits.CELSIUS); + if (celsiusTemperature != null) { + double doubleValue = celsiusTemperature.doubleValue(); + logger.trace("targetTemp double value = {}", doubleValue); + doubleValue = Math.max(minTemp, Math.min(maxTemp, doubleValue)); + value = String.format("%.0f", doubleValue * 10); + function = "SETPTEMP"; + logger.trace("targetTemp raw string = {}", value); + } + } + break; + case CHANNEL_TYPE_MODE: + function = "MODE"; + value = command.toString(); + break; + case CHANNEL_TYPE_FANSPEED: + function = "FANSP"; + value = command.toString(); + break; + case CHANNEL_TYPE_VANESUD: + function = "VANEUD"; + value = command.toString(); + break; + case CHANNEL_TYPE_VANESLR: + function = "VANELR"; + value = command.toString(); + break; + } + if (!value.isEmpty() || function.isEmpty()) { + if (api != null) { + logger.trace("Sending command {} to function {}", value, function); + api.sendCommand(function, value); + } else { + logger.warn("Sending command failed, could not get API"); + } + } + } + + private void populateProperties(String[] value) { + properties.put(PROPERTY_VENDOR, "Intesis"); + properties.put(PROPERTY_MODEL_ID, value[0]); + properties.put(PROPERTY_MAC_ADDRESS, value[1]); + properties.put("ipAddress", value[2]); + properties.put("protocol", value[3]); + properties.put(PROPERTY_FIRMWARE_VERSION, value[4]); + properties.put("hostname", value[6]); + updateProperties(properties); + hasProperties = true; + } + + private void receivedUpdate(String function, String receivedValue) { + String value = receivedValue; + logger.trace("receivedUpdate(): {} {}", function, value); + switch (function) { + case "ONOFF": + updateState(CHANNEL_TYPE_POWER, OnOffType.from(value)); + break; + + case "SETPTEMP": + if (value.equals("32768")) { + value = "0"; + } + updateState(CHANNEL_TYPE_TARGETTEMP, + new QuantityType(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS)); + break; + case "AMBTEMP": + if (Double.valueOf(value).isNaN()) { + value = "0"; + } + updateState(CHANNEL_TYPE_AMBIENTTEMP, + new QuantityType(Double.valueOf(value) / 10.0d, SIUnits.CELSIUS)); + break; + case "MODE": + updateState(CHANNEL_TYPE_MODE, new StringType(value)); + break; + case "FANSP": + updateState(CHANNEL_TYPE_FANSPEED, new StringType(value)); + break; + case "VANEUD": + updateState(CHANNEL_TYPE_VANESUD, new StringType(value)); + break; + case "VANELR": + updateState(CHANNEL_TYPE_VANESLR, new StringType(value)); + break; + case "ERRCODE": + properties.put("errorCode", value); + updateProperties(properties); + break; + case "ERRSTATUS": + properties.put("errorStatus", value); + updateProperties(properties); + if ("ERR".equals(value)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "device reported an error"); + } + break; + } + } + + private void handleMessage(String data) { + logger.debug("handleMessage(): Message received - {}", data); + if (data.equals("ACK") || data.equals("")) { + return; + } + if (data.startsWith(ID + ':')) { + String[] value = data.substring(3).split(","); + if (!hasProperties) { + populateProperties(value); + } + DecimalType signalStrength = mapSignalStrength(Integer.parseInt(value[5])); + updateState(CHANNEL_TYPE_RSSI, signalStrength); + return; + } + IntesisBoxMessage message = IntesisBoxMessage.parse(data); + if (message != null) { + switch (message.getCommand()) { + case LIMITS: + logger.debug("handleMessage(): Limits received - {}", data); + String function = message.getFunction(); + if (function.equals("SETPTEMP")) { + List limits = message.getLimitsValue().stream().map(l -> Double.valueOf(l) / 10.0d) + .collect(Collectors.toList()); + if (limits.size() == 2) { + minTemp = limits.get(0); + maxTemp = limits.get(1); + } + logger.trace("Property target temperatures {} added", message.getValue()); + properties.put("targetTemperature limits", "[" + minTemp + "," + maxTemp + "]"); + addChannel(CHANNEL_TYPE_TARGETTEMP, "Number:Temperature"); + } else { + switch (function) { + case "MODE": + properties.put("supported modes", message.getValue()); + limits.put(CHANNEL_TYPE_MODE, message.getLimitsValue()); + addChannel(CHANNEL_TYPE_MODE, "String"); + break; + case "FANSP": + properties.put("supported fan levels", message.getValue()); + limits.put(CHANNEL_TYPE_FANSPEED, message.getLimitsValue()); + addChannel(CHANNEL_TYPE_FANSPEED, "String"); + break; + case "VANEUD": + properties.put("supported vane up/down modes", message.getValue()); + limits.put(CHANNEL_TYPE_VANESUD, message.getLimitsValue()); + addChannel(CHANNEL_TYPE_VANESUD, "String"); + break; + case "VANELR": + properties.put("supported vane left/right modes", message.getValue()); + limits.put(CHANNEL_TYPE_VANESLR, message.getLimitsValue()); + addChannel(CHANNEL_TYPE_VANESLR, "String"); + break; + } + } + updateProperties(properties); + break; + case CHN: + receivedUpdate(message.getFunction(), message.getValue()); + break; + } + } + } + + public void addChannel(String channelId, String itemType) { + if (thing.getChannel(channelId) == null) { + logger.trace("Channel '{}' for UID to be added", channelId); + ThingBuilder thingBuilder = editThing(); + final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId); + Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), itemType) + .withType(channelTypeUID).withKind(ChannelKind.STATE).build(); + thingBuilder.withChannel(channel); + updateThing(thingBuilder.build()); + + if (limits.containsKey(channelId)) { + List options = new ArrayList<>(); + for (String mode : limits.get(channelId)) { + options.add(new StateOption(mode, + mode.substring(0, 1).toUpperCase() + mode.substring(1).toLowerCase())); + } + intesisStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), + options); + } + } + } + + @Override + public void messageReceived(String messageLine) { + logger.trace("messageReceived() : {}", messageLine); + handleMessage(messageLine); + } + + @Override + public void connectionStatusChanged(ThingStatus status, @Nullable String message) { + if (message != null) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + } + this.updateStatus(status); + } + + public static DecimalType mapSignalStrength(int dbm) { + int strength = -1; + if (dbm > -60) { + strength = 4; + } else if (dbm > -70) { + strength = 3; + } else if (dbm > -80) { + strength = 2; + } else if (dbm > -90) { + strength = 1; + } else { + strength = 0; + } + return new DecimalType(strength); + } +} diff --git a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisHomeHandler.java b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisHomeHandler.java index 9dbb2e26a3a98..194a50db15c6e 100644 --- a/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisHomeHandler.java +++ b/bundles/org.openhab.binding.intesis/src/main/java/org/openhab/binding/intesis/internal/handler/IntesisHomeHandler.java @@ -29,10 +29,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.intesis.internal.IntesisConfiguration; import org.openhab.binding.intesis.internal.IntesisDynamicStateDescriptionProvider; -import org.openhab.binding.intesis.internal.IntesisHomeModeEnum; import org.openhab.binding.intesis.internal.api.IntesisHomeHttpApi; +import org.openhab.binding.intesis.internal.config.IntesisHomeConfiguration; +import org.openhab.binding.intesis.internal.enums.IntesisHomeModeEnum; import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Data; import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Datapoints; import org.openhab.binding.intesis.internal.gson.IntesisHomeJSonDTO.Descr; @@ -83,7 +83,7 @@ public class IntesisHomeHandler extends BaseThingHandler { private final Gson gson = new Gson(); - private IntesisConfiguration config = new IntesisConfiguration(); + private IntesisHomeConfiguration config = new IntesisHomeConfiguration(); private @Nullable ScheduledFuture refreshJob; @@ -97,7 +97,7 @@ public IntesisHomeHandler(final Thing thing, final HttpClient httpClient, @Override public void initialize() { updateStatus(ThingStatus.UNKNOWN); - config = getConfigAs(IntesisConfiguration.class); + config = getConfigAs(IntesisHomeConfiguration.class); if (config.ipAddress.isEmpty() && config.password.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP-Address and password not set"); return; @@ -336,7 +336,7 @@ private void handleDataPointsResponse(Response response) { break; } } - properties.put("Supported modes", opModes.toString()); + properties.put("supported modes", opModes.toString()); channelId = CHANNEL_TYPE_MODE; addChannel(channelId, itemType, opModes); break; @@ -349,7 +349,7 @@ private void handleDataPointsResponse(Response response) { fanLevels.add(fanString); } } - properties.put("Supported fan levels", fanLevels.toString()); + properties.put("supported fan levels", fanLevels.toString()); channelId = CHANNEL_TYPE_FANSPEED; addChannel(channelId, itemType, fanLevels); break; @@ -372,12 +372,12 @@ private void handleDataPointsResponse(Response response) { switch (datapoint.uid) { case 5: channelId = CHANNEL_TYPE_VANESUD; - properties.put("Supported vane up/down modes", swingModes.toString()); + properties.put("supported vane up/down modes", swingModes.toString()); addChannel(channelId, itemType, swingModes); break; case 6: channelId = CHANNEL_TYPE_VANESLR; - properties.put("Supported vane left/right modes", swingModes.toString()); + properties.put("supported vane left/right modes", swingModes.toString()); addChannel(channelId, itemType, swingModes); break; } diff --git a/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/i18n/intesis.properties b/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/i18n/intesis.properties index cf808f1770fe2..2a04e56a09a75 100644 --- a/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/i18n/intesis.properties +++ b/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/i18n/intesis.properties @@ -29,7 +29,7 @@ channel-type.intesis.ambientTemperature.label = Ambient Temperature channel-type.intesis.ambientTemperature.description = Shows actual room temperature. channel-type.intesis.outdoorTemperature.label = Outdoor Temperature channel-type.intesis.outdoorTemperature.description = Shows actual outdoor temperature. -channel-type.intesis.fanSpeed.label = Wind Speed +channel-type.intesis.fanSpeed.label = Fan Speed channel-type.intesis.fanSpeed.description = Sets the fan speed on the Air conditioner. channel-type.intesis.fanSpeed.state.option.auto = Auto channel-type.intesis.vanesUpDown.label = Vertical Swing Mode @@ -40,3 +40,7 @@ channel-type.intesis.vanes.option.auto = AUTO channel-type.intesis.vanes.option.swing = Swing channel-type.intesis.vanes.option.swirl = Swirl channel-type.intesis.vanes.option.wide = Wide +channel-type.intesis.errorCode.label = Error Code +channel-type.intesis.errorCode.description = Shows the Air Conditioners error code if an error was found. +channel-type.intesis.errorStatus.label = Error Status +channel-type.intesis.errorStatus.description = Indicates if the Air Conditioner has encountered an error. diff --git a/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/i18n/intesis_de.properties b/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/i18n/intesis_de.properties index de2be2f9cda98..323f31bb55ce1 100644 --- a/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/i18n/intesis_de.properties +++ b/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/i18n/intesis_de.properties @@ -40,3 +40,8 @@ channel-type.intesis.vanes.option.auto = Auto channel-type.intesis.vanes.option.swing = Schwingen channel-type.intesis.vanes.option.swirl = Pulsieren channel-type.intesis.vanes.option.wide = Breit +channel-type.intesis.errorCode.label = Fehlercode +channel-type.intesis.errorCode.description = Zeigt im Fehlerzustand den Fehlercode an. +channel-type.intesis.errorStatus.label = Fehlerstatus +channel-type.intesis.errorStatus.description = Zeigt an, ob sich der Air Conditioner im Zustand "Fehler" befindet. + diff --git a/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/thing/thing-types.xml index 2540526351914..f8bd16002aa58 100644 --- a/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.intesis/src/main/resources/OH-INF/thing/thing-types.xml @@ -23,4 +23,48 @@ + + + + Represents a single IntesisBox adapter on the network, connected to an A/C unit. + + + + + + + + + + + @text/thing-type.config.intesis.ipAddress.description + network-address + + + + The TCP port to the IntesisBox. + 3310 + + + + + + Number:Temperature + + @text/channel-type.intesis.ambientTemperature.description + + + + + String + + @text/channel-type.intesis.errorCode.description + + + + String + + @text/channel-type.intesis.errorStatus.description + +