added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.venstarthermostat/.classpath
Normal file
32
bundles/org.openhab.binding.venstarthermostat/.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="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="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="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.venstarthermostat/.project
Normal file
23
bundles/org.openhab.binding.venstarthermostat/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.venstarthermostat</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>
|
||||
89
bundles/org.openhab.binding.venstarthermostat/README.md
Normal file
89
bundles/org.openhab.binding.venstarthermostat/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Venstar Thermostat Binding
|
||||
|
||||
The Venstar Thermostat binding supports an interface to WiFi enabled ColorTouch and Explorer thermostats manufactured by [Venstar](http://www.venstar.com).
|
||||
|
||||
Venstar WiFi enabled thermostats provide a local API that this binding uses
|
||||
to communicate with the thermostat. This binding does not require "cloud"
|
||||
access and may be used independently of Venstar's Skyport cloud services.
|
||||
|
||||
The Local API is not enabled by default, so you will need to set up your
|
||||
thermostat by configuring its WiFi connection and enabling the Local API. In
|
||||
order for the binding to connect, you will need to enable HTTPS support and
|
||||
set a username and password. While it is possible to enable the Local API
|
||||
without HTTPS and authentication, the binding doesn't support it, in an effort
|
||||
to provide as secure an installation as possible.
|
||||
|
||||
When you've set the username and password, make a note of these, as you'll need
|
||||
to enter them in the thermostat configuration in openHAB.
|
||||
|
||||
## Supported Things
|
||||
|
||||
| Thing Type | Description |
|
||||
|----------------------|-----------------------------------------------------------------------------------|
|
||||
| colorTouchThermostat | A Venstar [ColorTouch](http://www.venstar.com/thermostats/colortouch/) thermostat |
|
||||
|
||||
## Discovery
|
||||
|
||||
Once the binding is installed it will attempt to auto discovery Venstar thermostats located on the local network.
|
||||
These will appear as Things in the system Inbox.
|
||||
After adding the Inbox item, enter the user name and password from the physical thermostat in the Thing's configuration.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
### ColorTouch Thermostat
|
||||
|
||||
| Parameter | Description | Required |
|
||||
|-----------|------------------------------------------------------------------------------|----------|
|
||||
| username | The username set on the thermostats configuration screen (typically 'admin') | yes |
|
||||
| password | The password set set on the thermostats configuration screen | yes |
|
||||
| url | URL of the thermostat in the format 'proto://host' | yes |
|
||||
| refresh | The frequency in which the binding will pool for update information | no |
|
||||
|
||||
### Channels
|
||||
|
||||
| Channel | Type | Description | Notes |
|
||||
|--------------------|--------------------|------------------------------|--------------------------------------------------------|
|
||||
| systemMode | String | System Mode | |
|
||||
| systemModeRaw | Number | System Mode Raw (Read Only) | 0 (Off) 1 (Heat) 2 (Cool) 3 (Auto) |
|
||||
| systemState | String | System State (Read Only) | |
|
||||
| systemStateRaw | Number | System State Raw (Read Only) | 0 (Idle) 1 (Heating) 2 (Cooling) 3 (Lockout) 4 (Error) |
|
||||
| heatingSetpoint | Number:Temperature | Heating Set Point | |
|
||||
| coolingSetpoint | Number:Temperature | Cooling Set Point | |
|
||||
| temperature | Number:Temperature | Current Temperature | |
|
||||
| outdoorTemperature | Number:Temperature | Outdoor Temperature | |
|
||||
| humidity | Number | Humidity | |
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
### thermostat.things
|
||||
|
||||
```
|
||||
Thing venstarthermostat:colorTouchThermostat:001122334455 "Venstar Thermostat (Guest)" [ username="admin", password="secret", url="https://192.168.1.100", refresh=30 ]
|
||||
```
|
||||
|
||||
### thermostat.items
|
||||
|
||||
|
||||
```
|
||||
Number:Temperature Guest_HVAC_Temperature "Temperature [%d °F]" {channel="venstarthermostat:colorTouchThermostat:001122334455:temperature"}
|
||||
Number:Temperature Guest_HVAC_HeatSetpoint "Heat Setpoint [%d °F]" {channel="venstarthermostat:colorTouchThermostat:001122334455:heatingSetpoint"}
|
||||
Number:Temperature Guest_HVAC_CoolSetpoint "Cool Setpoint [%d °F]" {channel="venstarthermostat:colorTouchThermostat:001122334455:coolingSetpoint"}
|
||||
Number Guest_HVAC_Mode "Mode [%s]" {channel="venstarthermostat:colorTouchThermostat:001122334455:systemMode"}
|
||||
Number Guest_HVAC_Humidity "Humidity [%d %%]" {channel="venstarthermostat:colorTouchThermostat:001122334455:humidity"}
|
||||
Number Guest_HVAC_State "State [%s]" {channel="venstarthermostat:colorTouchThermostat:001122334455:systemState"}
|
||||
```
|
||||
|
||||
### thermostat.sitemap
|
||||
|
||||
```
|
||||
sitemap demo label="Venstar Color Thermostat Demo"
|
||||
{
|
||||
Frame {
|
||||
Setpoint item=Guest_HVAC_HeatSetpoint minValue=50 maxValue=99
|
||||
Setpoint item=Guest_HVAC_CoolSetpoint minValue=50 maxValue=99
|
||||
Switch item=Guest_HVAC_Mode mappings=[off=Off,heat=Heat,cool=Cool,auto=Auto]
|
||||
Text item=Guest_HVAC_State
|
||||
}
|
||||
}
|
||||
```
|
||||
15
bundles/org.openhab.binding.venstarthermostat/pom.xml
Normal file
15
bundles/org.openhab.binding.venstarthermostat/pom.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?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.venstarthermostat</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Venstar Thermostat Binding</name>
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.ventarthermostat-${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-ventarthermostat" description="Venstar Thermostat Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.venstarthermostat/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link VenstarThermostatBinding} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class VenstarThermostatBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "venstarthermostat";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public final static ThingTypeUID THING_TYPE_COLOR_TOUCH = new ThingTypeUID(BINDING_ID, "colorTouchThermostat");
|
||||
|
||||
public final static Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COLOR_TOUCH);
|
||||
// List of all Channel ids
|
||||
public final static String CHANNEL_TEMPERATURE = "temperature";
|
||||
public final static String CHANNEL_HUMIDITY = "humidity";
|
||||
public final static String CHANNEL_EXTERNAL_TEMPERATURE = "outdoorTemperature";
|
||||
|
||||
public final static String CHANNEL_HEATING_SETPOINT = "heatingSetpoint";
|
||||
public final static String CHANNEL_COOLING_SETPOINT = "coolingSetpoint";
|
||||
public final static String CHANNEL_SYSTEM_STATE = "systemState";
|
||||
public final static String CHANNEL_SYSTEM_MODE = "systemMode";
|
||||
public final static String CHANNEL_SYSTEM_STATE_RAW = "systemStateRaw";
|
||||
public final static String CHANNEL_SYSTEM_MODE_RAW = "systemModeRaw";
|
||||
|
||||
public final static String CONFIG_USERNAME = "username";
|
||||
public final static String CONFIG_PASSWORD = "password";
|
||||
public final static String CONFIG_REFRESH = "refresh";
|
||||
|
||||
public final static String PROPERTY_URL = "url";
|
||||
public final static String PROPERTY_UUID = "uuid";
|
||||
|
||||
public final static String REFRESH_INVALID = "refresh-invalid";
|
||||
public final static String EMPTY_INVALID = "empty-invalid";
|
||||
}
|
||||
@@ -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.venstarthermostat.internal;
|
||||
|
||||
/**
|
||||
* The {@link VenstarThermostatConfiguration} is responsible for holding configuration information.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
|
||||
public class VenstarThermostatConfiguration {
|
||||
public String username;
|
||||
public String password;
|
||||
public String url;
|
||||
public Integer refresh;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal;
|
||||
|
||||
import static org.openhab.binding.venstarthermostat.internal.VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.venstarthermostat.internal.handler.VenstarThermostatHandler;
|
||||
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 VenstarThermostatHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.venstarthermostat")
|
||||
public class VenstarThermostatHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final static Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_COLOR_TOUCH);
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_COLOR_TOUCH)) {
|
||||
return new VenstarThermostatHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.discovery;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MulticastSocket;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.openhab.binding.venstarthermostat.internal.VenstarThermostatBindingConstants;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link VenstarThermostatDiscoveryService} is responsible for discovery of
|
||||
* Venstar thermostats on the local network
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
* @author Dan Cunningham - Refactoring and Improvements
|
||||
*/
|
||||
|
||||
@Component(service = DiscoveryService.class, configurationPid = "discovery.venstarthermostat")
|
||||
public class VenstarThermostatDiscoveryService extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(VenstarThermostatDiscoveryService.class);
|
||||
private static final String COLOR_TOUCH_DISCOVERY_MESSAGE = "M-SEARCH * HTTP/1.1\r\n"
|
||||
+ "Host: 239.255.255.250:1900\r\n" + "Man: ssdp:discover\r\n" + "ST: colortouch:ecp\r\n" + "\r\n";
|
||||
private static final Pattern USN_PATTERN = Pattern
|
||||
.compile("^(colortouch:)?ecp((?::[0-9a-fA-F]{2}){6}):name:(.+)(?::type:(\\w+))");
|
||||
private static final String SSDP_MATCH = "colortouch:ecp";
|
||||
private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
|
||||
|
||||
private ScheduledFuture<?> scheduledFuture = null;
|
||||
|
||||
public VenstarThermostatDiscoveryService() {
|
||||
super(VenstarThermostatBindingConstants.SUPPORTED_THING_TYPES, 30, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
logger.debug("Starting Background Scan");
|
||||
stopBackgroundDiscovery();
|
||||
scheduledFuture = scheduler.scheduleAtFixedRate(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
|
||||
scheduledFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting Interactive Scan");
|
||||
doRunRun();
|
||||
}
|
||||
|
||||
protected synchronized void doRunRun() {
|
||||
logger.trace("Sending SSDP discover.");
|
||||
for (int i = 0; i < 5; i++) {
|
||||
try {
|
||||
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
|
||||
while (nets.hasMoreElements()) {
|
||||
NetworkInterface ni = nets.nextElement();
|
||||
MulticastSocket socket = sendDiscoveryBroacast(ni);
|
||||
if (socket != null) {
|
||||
scanResposesForKeywords(socket);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.debug("Error discoverying devices", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a SSDP discovery message into the network to find provided
|
||||
* services.
|
||||
*
|
||||
* @return The Socket the answers will arrive at.
|
||||
* @throws UnknownHostException
|
||||
* @throws IOException
|
||||
* @throws SocketException
|
||||
* @throws UnsupportedEncodingException
|
||||
*/
|
||||
private MulticastSocket sendDiscoveryBroacast(NetworkInterface ni)
|
||||
throws UnknownHostException, SocketException, UnsupportedEncodingException {
|
||||
InetAddress m = InetAddress.getByName("239.255.255.250");
|
||||
final int port = 1900;
|
||||
|
||||
logger.trace("Considering {}", ni.getName());
|
||||
try {
|
||||
if (!ni.isUp() || !ni.supportsMulticast()) {
|
||||
logger.trace("skipping interface {}", ni.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
Enumeration<InetAddress> addrs = ni.getInetAddresses();
|
||||
InetAddress a = null;
|
||||
while (addrs.hasMoreElements()) {
|
||||
a = addrs.nextElement();
|
||||
if (a instanceof Inet4Address) {
|
||||
break;
|
||||
} else {
|
||||
a = null;
|
||||
}
|
||||
}
|
||||
if (a == null) {
|
||||
logger.trace("no ipv4 address on {}", ni.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
// for whatever reason, the venstar thermostat responses will not be seen
|
||||
// if we bind this socket to a particular address.
|
||||
// this seems to be okay on linux systems, but osx apparently prefers ipv6, so this
|
||||
// prevents responses from being received unless the ipv4 stack is given preference.
|
||||
MulticastSocket socket = new MulticastSocket(new InetSocketAddress(port));
|
||||
socket.setSoTimeout(2000);
|
||||
socket.setReuseAddress(true);
|
||||
socket.setNetworkInterface(ni);
|
||||
socket.joinGroup(m);
|
||||
|
||||
logger.trace("Joined UPnP Multicast group on Interface: {}", ni.getName());
|
||||
byte[] requestMessage = COLOR_TOUCH_DISCOVERY_MESSAGE.getBytes("UTF-8");
|
||||
DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
|
||||
socket.send(datagramPacket);
|
||||
return socket;
|
||||
} catch (IOException e) {
|
||||
logger.trace("got ioexception: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans all messages that arrive on the socket and scans them for the
|
||||
* search keywords. The search is not case sensitive.
|
||||
*
|
||||
* @param socket
|
||||
* The socket where the answers arrive.
|
||||
* @param keywords
|
||||
* The keywords to be searched for.
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private void scanResposesForKeywords(MulticastSocket socket, String... keywords) throws IOException {
|
||||
// In the worst case a SocketTimeoutException raises
|
||||
do {
|
||||
byte[] rxbuf = new byte[8192];
|
||||
DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
|
||||
try {
|
||||
socket.receive(packet);
|
||||
} catch (IOException e) {
|
||||
logger.trace("Got exception while trying to receive UPnP packets: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
String response = new String(packet.getData());
|
||||
if (response.contains(SSDP_MATCH)) {
|
||||
logger.trace("Match: {} ", response);
|
||||
parseResponse(response);
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
protected void parseResponse(String response) {
|
||||
DiscoveryResult result;
|
||||
|
||||
String name = null;
|
||||
String url = null;
|
||||
String uuid = null;
|
||||
|
||||
Scanner scanner = new Scanner(response);
|
||||
while (scanner.hasNextLine()) {
|
||||
String line = scanner.nextLine();
|
||||
String[] pair = line.split(":", 2);
|
||||
if (pair.length != 2) {
|
||||
continue;
|
||||
}
|
||||
String key = pair[0].toLowerCase();
|
||||
String value = pair[1].trim();
|
||||
logger.trace("key: {} value: {}.", key, value);
|
||||
switch (key) {
|
||||
case "location":
|
||||
url = value;
|
||||
break;
|
||||
case "usn":
|
||||
Matcher m = USN_PATTERN.matcher(value);
|
||||
if (m.find()) {
|
||||
uuid = m.group(2);
|
||||
name = m.group(3);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
scanner.close();
|
||||
|
||||
logger.trace("Found thermostat, name: {} uuid: {} url: {}", name, uuid, url);
|
||||
|
||||
if (name == null || uuid == null || url == null) {
|
||||
logger.trace("Bad Format from thermostat");
|
||||
return;
|
||||
}
|
||||
|
||||
uuid = uuid.replace(":", "").toLowerCase();
|
||||
|
||||
ThingUID thingUid = new ThingUID(VenstarThermostatBindingConstants.THING_TYPE_COLOR_TOUCH, uuid);
|
||||
|
||||
logger.trace("Got discovered device.");
|
||||
|
||||
String label = String.format("Venstar Thermostat (%s)", name);
|
||||
result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
|
||||
.withRepresentationProperty(VenstarThermostatBindingConstants.PROPERTY_UUID)
|
||||
.withProperty(VenstarThermostatBindingConstants.PROPERTY_UUID, uuid)
|
||||
.withProperty(VenstarThermostatBindingConstants.PROPERTY_URL, url).build();
|
||||
logger.trace("New venstar thermostat discovered with ID=<{}>", uuid);
|
||||
this.thingDiscovered(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.handler;
|
||||
|
||||
import static org.openhab.binding.venstarthermostat.internal.VenstarThermostatBindingConstants.*;
|
||||
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.DigestAuthentication;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.openhab.binding.venstarthermostat.internal.VenstarThermostatConfiguration;
|
||||
import org.openhab.binding.venstarthermostat.internal.model.VenstarInfoData;
|
||||
import org.openhab.binding.venstarthermostat.internal.model.VenstarResponse;
|
||||
import org.openhab.binding.venstarthermostat.internal.model.VenstarSensor;
|
||||
import org.openhab.binding.venstarthermostat.internal.model.VenstarSensorData;
|
||||
import org.openhab.binding.venstarthermostat.internal.model.VenstarSystemMode;
|
||||
import org.openhab.binding.venstarthermostat.internal.model.VenstarSystemModeSerializer;
|
||||
import org.openhab.binding.venstarthermostat.internal.model.VenstarSystemState;
|
||||
import org.openhab.binding.venstarthermostat.internal.model.VenstarSystemStateSerializer;
|
||||
import org.openhab.core.config.core.status.ConfigStatusMessage;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
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.ConfigStatusThingHandler;
|
||||
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;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* The {@link VenstarThermostatHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
* @author Dan Cunningham - Migration to Jetty, annotations and various improvements
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class VenstarThermostatHandler extends ConfigStatusThingHandler {
|
||||
|
||||
private static final int TIMEOUT_SECONDS = 30;
|
||||
private static final int UPDATE_AFTER_COMMAND_SECONDS = 2;
|
||||
|
||||
private Logger log = LoggerFactory.getLogger(VenstarThermostatHandler.class);
|
||||
private List<VenstarSensor> sensorData = new ArrayList<>();
|
||||
private VenstarInfoData infoData = new VenstarInfoData();
|
||||
private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
|
||||
private @Nullable Future<?> updatesTask;
|
||||
private @Nullable URL baseURL;
|
||||
private int refresh;
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
|
||||
// Venstar Thermostats are most commonly installed in the US, so start with a reasonable default.
|
||||
private Unit<Temperature> unitSystem = ImperialUnits.FAHRENHEIT;
|
||||
|
||||
public VenstarThermostatHandler(Thing thing) {
|
||||
super(thing);
|
||||
httpClient = new HttpClient(new SslContextFactory(true));
|
||||
gson = new GsonBuilder().registerTypeAdapter(VenstarSystemState.class, new VenstarSystemStateSerializer())
|
||||
.registerTypeAdapter(VenstarSystemMode.class, new VenstarSystemModeSerializer()).create();
|
||||
|
||||
log.trace("VenstarThermostatHandler for thing {}", getThing().getUID());
|
||||
}
|
||||
|
||||
@SuppressWarnings("null") // compiler does not see conf.refresh == null check
|
||||
@Override
|
||||
public Collection<ConfigStatusMessage> getConfigStatus() {
|
||||
Collection<ConfigStatusMessage> status = new ArrayList<>();
|
||||
VenstarThermostatConfiguration config = getConfigAs(VenstarThermostatConfiguration.class);
|
||||
if (StringUtils.isBlank(config.username)) {
|
||||
log.warn("username is empty");
|
||||
status.add(ConfigStatusMessage.Builder.error(CONFIG_USERNAME).withMessageKeySuffix(EMPTY_INVALID)
|
||||
.withArguments(CONFIG_USERNAME).build());
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(config.password)) {
|
||||
log.warn("password is empty");
|
||||
status.add(ConfigStatusMessage.Builder.error(CONFIG_PASSWORD).withMessageKeySuffix(EMPTY_INVALID)
|
||||
.withArguments(CONFIG_PASSWORD).build());
|
||||
}
|
||||
|
||||
if (config.refresh == null || config.refresh < 10) {
|
||||
log.warn("refresh is too small: {}", config.refresh);
|
||||
|
||||
status.add(ConfigStatusMessage.Builder.error(CONFIG_REFRESH).withMessageKeySuffix(REFRESH_INVALID)
|
||||
.withArguments(CONFIG_REFRESH).build());
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
log.debug("Controller is NOT ONLINE and is not responding to commands");
|
||||
return;
|
||||
}
|
||||
|
||||
stopUpdateTasks();
|
||||
if (command instanceof RefreshType) {
|
||||
log.debug("Refresh command requested for {}", channelUID);
|
||||
stateMap.clear();
|
||||
startUpdatesTask(0);
|
||||
} else {
|
||||
stateMap.remove(channelUID.getAsString());
|
||||
if (channelUID.getId().equals(CHANNEL_HEATING_SETPOINT)) {
|
||||
QuantityType<Temperature> quantity = commandToQuantityType(command, unitSystem);
|
||||
int value = quantityToRoundedTemperature(quantity, unitSystem).intValue();
|
||||
log.debug("Setting heating setpoint to {}", value);
|
||||
setHeatingSetpoint(value);
|
||||
} else if (channelUID.getId().equals(CHANNEL_COOLING_SETPOINT)) {
|
||||
QuantityType<Temperature> quantity = commandToQuantityType(command, unitSystem);
|
||||
int value = quantityToRoundedTemperature(quantity, unitSystem).intValue();
|
||||
log.debug("Setting cooling setpoint to {}", value);
|
||||
setCoolingSetpoint(value);
|
||||
} else if (channelUID.getId().equals(CHANNEL_SYSTEM_MODE)) {
|
||||
VenstarSystemMode value;
|
||||
if (command instanceof StringType) {
|
||||
value = VenstarSystemMode.valueOf(((StringType) command).toString().toUpperCase());
|
||||
} else {
|
||||
value = VenstarSystemMode.fromInt(((DecimalType) command).intValue());
|
||||
}
|
||||
log.debug("Setting system mode to {}", value);
|
||||
setSystemMode(value);
|
||||
updateIfChanged(CHANNEL_SYSTEM_MODE_RAW, new StringType("" + value));
|
||||
}
|
||||
startUpdatesTask(UPDATE_AFTER_COMMAND_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopUpdateTasks();
|
||||
if (httpClient.isStarted()) {
|
||||
try {
|
||||
httpClient.stop();
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not stop HttpClient", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
connect();
|
||||
}
|
||||
|
||||
protected void goOnline() {
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
|
||||
protected void goOffline(ThingStatusDetail detail, String reason) {
|
||||
if (getThing().getStatus() != ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, detail, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("null") // compiler does not see new URL(url) as never being null
|
||||
private void connect() {
|
||||
stopUpdateTasks();
|
||||
VenstarThermostatConfiguration config = getConfigAs(VenstarThermostatConfiguration.class);
|
||||
try {
|
||||
baseURL = new URL(config.url);
|
||||
if (!httpClient.isStarted()) {
|
||||
httpClient.start();
|
||||
}
|
||||
httpClient.getAuthenticationStore().clearAuthentications();
|
||||
httpClient.getAuthenticationStore().clearAuthenticationResults();
|
||||
httpClient.getAuthenticationStore().addAuthentication(
|
||||
new DigestAuthentication(baseURL.toURI(), "thermostat", config.username, config.password));
|
||||
refresh = config.refresh;
|
||||
startUpdatesTask(0);
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not conntect to URL {}", config.url, e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the poller after an initial delay
|
||||
*
|
||||
* @param initialDelay
|
||||
*/
|
||||
private synchronized void startUpdatesTask(int initialDelay) {
|
||||
stopUpdateTasks();
|
||||
updatesTask = scheduler.scheduleWithFixedDelay(this::updateData, initialDelay, refresh, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the poller
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
private void stopUpdateTasks() {
|
||||
Future<?> localUpdatesTask = updatesTask;
|
||||
if (isFutureValid(localUpdatesTask)) {
|
||||
localUpdatesTask.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFutureValid(@Nullable Future<?> future) {
|
||||
return future != null && !future.isCancelled();
|
||||
}
|
||||
|
||||
private State getTemperature() {
|
||||
Optional<VenstarSensor> optSensor = sensorData.stream()
|
||||
.filter(sensor -> sensor.getName().equalsIgnoreCase("Thermostat")).findAny();
|
||||
if (optSensor.isPresent()) {
|
||||
return new QuantityType<Temperature>(optSensor.get().getTemp(), unitSystem);
|
||||
}
|
||||
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
private State getHumidity() {
|
||||
Optional<VenstarSensor> optSensor = sensorData.stream()
|
||||
.filter(sensor -> sensor.getName().equalsIgnoreCase("Thermostat")).findAny();
|
||||
if (optSensor.isPresent()) {
|
||||
return new QuantityType<Dimensionless>(optSensor.get().getHum(), SmartHomeUnits.PERCENT);
|
||||
}
|
||||
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
private State getOutdoorTemperature() {
|
||||
Optional<VenstarSensor> optSensor = sensorData.stream()
|
||||
.filter(sensor -> sensor.getName().equalsIgnoreCase("Outdoor")).findAny();
|
||||
if (optSensor.isPresent()) {
|
||||
return new QuantityType<Temperature>(optSensor.get().getTemp(), unitSystem);
|
||||
}
|
||||
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
|
||||
private void setCoolingSetpoint(int cool) {
|
||||
int heat = getHeatingSetpoint().intValue();
|
||||
VenstarSystemMode mode = getSystemMode();
|
||||
updateThermostat(heat, cool, mode);
|
||||
}
|
||||
|
||||
private void setSystemMode(VenstarSystemMode mode) {
|
||||
int cool = getCoolingSetpoint().intValue();
|
||||
int heat = getHeatingSetpoint().intValue();
|
||||
updateThermostat(heat, cool, mode);
|
||||
}
|
||||
|
||||
private void setHeatingSetpoint(int heat) {
|
||||
int cool = getCoolingSetpoint().intValue();
|
||||
VenstarSystemMode mode = getSystemMode();
|
||||
updateThermostat(heat, cool, mode);
|
||||
}
|
||||
|
||||
private QuantityType<Temperature> getCoolingSetpoint() {
|
||||
return new QuantityType<Temperature>(infoData.getCooltemp(), unitSystem);
|
||||
}
|
||||
|
||||
private QuantityType<Temperature> getHeatingSetpoint() {
|
||||
return new QuantityType<Temperature>(infoData.getHeattemp(), unitSystem);
|
||||
}
|
||||
|
||||
private VenstarSystemState getSystemState() {
|
||||
return infoData.getState();
|
||||
}
|
||||
|
||||
private VenstarSystemMode getSystemMode() {
|
||||
return infoData.getMode();
|
||||
}
|
||||
|
||||
private void updateThermostat(int heat, int cool, VenstarSystemMode mode) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
log.debug("Updating thermostat {} heat:{} cool {} mode: {}", getThing().getLabel(), heat, cool, mode);
|
||||
if (heat > 0) {
|
||||
params.put("heattemp", String.valueOf(heat));
|
||||
}
|
||||
if (cool > 0) {
|
||||
params.put("cooltemp", String.valueOf(cool));
|
||||
}
|
||||
params.put("mode", "" + mode.mode());
|
||||
try {
|
||||
String result = postData("/control", params);
|
||||
VenstarResponse res = gson.fromJson(result, VenstarResponse.class);
|
||||
if (res.isSuccess()) {
|
||||
log.debug("Updated thermostat");
|
||||
// update our local copy until the next refresh occurs
|
||||
infoData = new VenstarInfoData(cool, heat, infoData.getState(), mode);
|
||||
} else {
|
||||
log.debug("Failed to update thermostat: {}", res.getReason());
|
||||
goOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Thermostat update failed: " + res.getReason());
|
||||
}
|
||||
} catch (VenstarCommunicationException | JsonSyntaxException e) {
|
||||
log.debug("Unable to fetch info data", e);
|
||||
goOffline(ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
} catch (VenstarAuthenticationException e) {
|
||||
goOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Authorization Failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateData() {
|
||||
try {
|
||||
Future<?> localUpdatesTask = updatesTask;
|
||||
String response = getData("/query/sensors");
|
||||
if (!isFutureValid(localUpdatesTask)) {
|
||||
return;
|
||||
}
|
||||
VenstarSensorData res = gson.fromJson(response, VenstarSensorData.class);
|
||||
sensorData = res.getSensors();
|
||||
updateIfChanged(CHANNEL_TEMPERATURE, getTemperature());
|
||||
updateIfChanged(CHANNEL_EXTERNAL_TEMPERATURE, getOutdoorTemperature());
|
||||
updateIfChanged(CHANNEL_HUMIDITY, getHumidity());
|
||||
|
||||
response = getData("/query/info");
|
||||
if (!isFutureValid(localUpdatesTask)) {
|
||||
return;
|
||||
}
|
||||
infoData = gson.fromJson(response, VenstarInfoData.class);
|
||||
updateUnits(infoData);
|
||||
updateIfChanged(CHANNEL_HEATING_SETPOINT, getHeatingSetpoint());
|
||||
updateIfChanged(CHANNEL_COOLING_SETPOINT, getCoolingSetpoint());
|
||||
updateIfChanged(CHANNEL_SYSTEM_STATE, new StringType(getSystemState().stateName()));
|
||||
updateIfChanged(CHANNEL_SYSTEM_MODE, new StringType(getSystemMode().modeName()));
|
||||
updateIfChanged(CHANNEL_SYSTEM_STATE_RAW, new DecimalType(getSystemState().state()));
|
||||
updateIfChanged(CHANNEL_SYSTEM_MODE_RAW, new DecimalType(getSystemMode().mode()));
|
||||
|
||||
goOnline();
|
||||
} catch (VenstarCommunicationException | JsonSyntaxException e) {
|
||||
log.debug("Unable to fetch info data", e);
|
||||
goOffline(ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
} catch (VenstarAuthenticationException e) {
|
||||
goOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Authorization Failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateIfChanged(String channelID, State state) {
|
||||
ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelID);
|
||||
State oldState = stateMap.put(channelUID.toString(), state);
|
||||
if (!state.equals(oldState)) {
|
||||
log.trace("updating channel {} with state {} (old state {})", channelUID, state, oldState);
|
||||
updateState(channelUID, state);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUnits(VenstarInfoData infoData) {
|
||||
int tempunits = infoData.getTempunits();
|
||||
if (tempunits == 0) {
|
||||
unitSystem = ImperialUnits.FAHRENHEIT;
|
||||
} else if (tempunits == 1) {
|
||||
unitSystem = SIUnits.CELSIUS;
|
||||
} else {
|
||||
log.warn("Thermostat returned unknown unit system type: {}", tempunits);
|
||||
}
|
||||
}
|
||||
|
||||
private String getData(String path) throws VenstarAuthenticationException, VenstarCommunicationException {
|
||||
try {
|
||||
URL getURL = new URL(baseURL, path);
|
||||
Request request = httpClient.newRequest(getURL.toURI()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
return sendRequest(request);
|
||||
} catch (MalformedURLException | URISyntaxException e) {
|
||||
throw new VenstarCommunicationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String postData(String path, Map<String, String> params)
|
||||
throws VenstarAuthenticationException, VenstarCommunicationException {
|
||||
try {
|
||||
URL postURL = new URL(baseURL, path);
|
||||
Request request = httpClient.newRequest(postURL.toURI()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.method(HttpMethod.POST);
|
||||
params.forEach(request::param);
|
||||
return sendRequest(request);
|
||||
} catch (MalformedURLException | URISyntaxException e) {
|
||||
throw new VenstarCommunicationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String sendRequest(Request request) throws VenstarAuthenticationException, VenstarCommunicationException {
|
||||
log.trace("sendRequest: requesting {}", request.getURI());
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
log.trace("Response code {}", response.getStatus());
|
||||
if (response.getStatus() == 401) {
|
||||
throw new VenstarAuthenticationException();
|
||||
}
|
||||
|
||||
if (response.getStatus() != 200) {
|
||||
throw new VenstarCommunicationException(
|
||||
"Error communitcating with thermostat. Error Code: " + response.getStatus());
|
||||
}
|
||||
String content = response.getContentAsString();
|
||||
log.trace("sendRequest: response {}", content);
|
||||
return content;
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException e) {
|
||||
throw new VenstarCommunicationException(e);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <U extends Quantity<U>> QuantityType<U> commandToQuantityType(Command command, Unit<U> defaultUnit) {
|
||||
if (command instanceof QuantityType) {
|
||||
return (QuantityType<U>) command;
|
||||
}
|
||||
return new QuantityType<U>(new BigDecimal(command.toString()), defaultUnit);
|
||||
}
|
||||
|
||||
protected DecimalType commandToDecimalType(Command command) {
|
||||
if (command instanceof DecimalType) {
|
||||
return (DecimalType) command;
|
||||
}
|
||||
return new DecimalType(new BigDecimal(command.toString()));
|
||||
}
|
||||
|
||||
private BigDecimal quantityToRoundedTemperature(QuantityType<Temperature> quantity, Unit<Temperature> unit)
|
||||
throws IllegalArgumentException {
|
||||
QuantityType<Temperature> temparatureQuantity = quantity.toUnit(unit);
|
||||
if (temparatureQuantity == null) {
|
||||
return quantity.toBigDecimal();
|
||||
}
|
||||
|
||||
BigDecimal value = temparatureQuantity.toBigDecimal();
|
||||
BigDecimal increment = CELSIUS == unit ? new BigDecimal("0.5") : new BigDecimal("1");
|
||||
BigDecimal divisor = value.divide(increment, 0, RoundingMode.HALF_UP);
|
||||
return divisor.multiply(increment);
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private class VenstarAuthenticationException extends Exception {
|
||||
public VenstarAuthenticationException() {
|
||||
super("Invalid Credentials");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private class VenstarCommunicationException extends Exception {
|
||||
public VenstarCommunicationException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
public VenstarCommunicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.model;
|
||||
|
||||
/**
|
||||
* The {@link VenstarInfoData} represents a thermostat state from the REST API.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
public class VenstarInfoData {
|
||||
double cooltemp;
|
||||
double heattemp;
|
||||
|
||||
VenstarSystemState state;
|
||||
VenstarSystemMode mode;
|
||||
int tempunits;
|
||||
|
||||
public VenstarInfoData() {
|
||||
super();
|
||||
}
|
||||
|
||||
public VenstarInfoData(double cooltemp, double heattemp, VenstarSystemState state, VenstarSystemMode mode) {
|
||||
super();
|
||||
this.cooltemp = cooltemp;
|
||||
this.heattemp = heattemp;
|
||||
this.state = state;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public double getCooltemp() {
|
||||
return cooltemp;
|
||||
}
|
||||
|
||||
public void setCooltemp(double cooltemp) {
|
||||
this.cooltemp = cooltemp;
|
||||
}
|
||||
|
||||
public double getHeattemp() {
|
||||
return heattemp;
|
||||
}
|
||||
|
||||
public void setHeattemp(double heattemp) {
|
||||
this.heattemp = heattemp;
|
||||
}
|
||||
|
||||
public VenstarSystemState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(VenstarSystemState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public VenstarSystemMode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(VenstarSystemMode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public int getTempunits() {
|
||||
return tempunits;
|
||||
}
|
||||
|
||||
public void setTempunits(int tempunits) {
|
||||
this.tempunits = tempunits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.model;
|
||||
|
||||
/**
|
||||
* The {@link VenstarResponse} represents a response message from the REST API.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
public class VenstarResponse {
|
||||
|
||||
private boolean success;
|
||||
private String reason;
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(boolean success) {
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.venstarthermostat.internal.model;
|
||||
|
||||
/**
|
||||
* The {@link VenstarSensor} represents a sensor returned from the REST API.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
public class VenstarSensor {
|
||||
String name;
|
||||
float temp;
|
||||
float hum;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public float getTemp() {
|
||||
return temp;
|
||||
}
|
||||
|
||||
public void setTemp(float temp) {
|
||||
this.temp = temp;
|
||||
}
|
||||
|
||||
public float getHum() {
|
||||
return hum;
|
||||
}
|
||||
|
||||
public void setHum(float hum) {
|
||||
this.hum = hum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The {@link VenstarSensorData} represents sensor data returned from the REST API.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
public class VenstarSensorData {
|
||||
List<VenstarSensor> sensors;
|
||||
|
||||
public List<VenstarSensor> getSensors() {
|
||||
return sensors;
|
||||
}
|
||||
|
||||
public void setSensors(List<VenstarSensor> sensors) {
|
||||
this.sensors = sensors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.model;
|
||||
|
||||
/**
|
||||
* The {@link VenstarSystemMode} represents the value of the system mode returned
|
||||
* from the REST API.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
public enum VenstarSystemMode {
|
||||
OFF(0, "off", "Off"),
|
||||
HEAT(1, "heat", "Heat"),
|
||||
COOL(2, "cool", "Cool"),
|
||||
AUTO(3, "auto", "Auto");
|
||||
|
||||
private int mode;
|
||||
private String name;
|
||||
private String friendlyName;
|
||||
|
||||
VenstarSystemMode(int mode, String name, String friendlyName) {
|
||||
this.mode = mode;
|
||||
this.name = name;
|
||||
this.friendlyName = friendlyName;
|
||||
}
|
||||
|
||||
public int mode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public String modeName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String friendlyName() {
|
||||
return friendlyName;
|
||||
}
|
||||
|
||||
public static VenstarSystemMode fromInt(int mode) {
|
||||
for (VenstarSystemMode sm : values()) {
|
||||
if (sm.mode == mode) {
|
||||
return sm;
|
||||
}
|
||||
}
|
||||
|
||||
throw (new IllegalArgumentException("Invalid system mode " + mode));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.model;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* The {@link VenstarSystemModeSerializer} parses system mode values
|
||||
* from the REST API JSON.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
public class VenstarSystemModeSerializer implements JsonDeserializer<VenstarSystemMode> {
|
||||
@Override
|
||||
public VenstarSystemMode deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
|
||||
throws JsonParseException {
|
||||
int key = element.getAsInt();
|
||||
return VenstarSystemMode.fromInt(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.model;
|
||||
|
||||
/**
|
||||
* The {@link VenstarSystemState} represents the value of the system state
|
||||
* returneda from the REST API.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
public enum VenstarSystemState {
|
||||
IDLE(0, "idle", "Idle"),
|
||||
HEATING(1, "heating", "Heating"),
|
||||
COOLING(2, "cooling", "Cooling"),
|
||||
LOCKOUT(3, "lockout", "Lockout"),
|
||||
ERROR(4, "error", "Error");
|
||||
|
||||
private int state;
|
||||
private String name;
|
||||
private String friendlyName;
|
||||
|
||||
VenstarSystemState(int state, String name, String friendlyName) {
|
||||
this.state = state;
|
||||
this.name = name;
|
||||
this.friendlyName = friendlyName;
|
||||
}
|
||||
|
||||
public int state() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String stateName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String friendlyName() {
|
||||
return friendlyName;
|
||||
}
|
||||
|
||||
public static VenstarSystemState fromInt(int state) {
|
||||
for (VenstarSystemState ss : values()) {
|
||||
if (ss.state == state) {
|
||||
return ss;
|
||||
}
|
||||
}
|
||||
|
||||
throw (new IllegalArgumentException("Invalid system state " + state));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.venstarthermostat.internal.model;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* The {@link VenstarSystemStateSerializer} parses system state values
|
||||
* from the REST API JSON.
|
||||
*
|
||||
* @author William Welliver - Initial contribution
|
||||
*/
|
||||
public class VenstarSystemStateSerializer implements JsonDeserializer<VenstarSystemState> {
|
||||
@Override
|
||||
public VenstarSystemState deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2)
|
||||
throws JsonParseException {
|
||||
int key = element.getAsInt();
|
||||
return VenstarSystemState.fromInt(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="venstarthermostat" 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>Venstar Thermostats</name>
|
||||
<description>This is a binding for Venstar Thermostats.</description>
|
||||
<author>William Welliver</author>
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="venstarthermostat"
|
||||
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">
|
||||
|
||||
<!-- Sample Thing Type -->
|
||||
<thing-type id="colorTouchThermostat">
|
||||
<label>ColorTouch Thermostat</label>
|
||||
<description>Venstar ColorTouch Thermostat</description>
|
||||
|
||||
<channels>
|
||||
<channel id="temperature" typeId="temperature"/>
|
||||
<channel id="humidity" typeId="humidity"/>
|
||||
<channel id="outdoorTemperature" typeId="outdoorTemperature"/>
|
||||
<channel id="systemMode" typeId="systemMode"/>
|
||||
<channel id="systemModeRaw" typeId="systemModeRaw"/>
|
||||
<channel id="heatingSetpoint" typeId="heatingSetpoint"/>
|
||||
<channel id="coolingSetpoint" typeId="coolingSetpoint"/>
|
||||
<channel id="systemState" typeId="systemState"/>
|
||||
<channel id="systemStateRaw" typeId="systemStateRaw"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="uuid"></property>
|
||||
</properties>
|
||||
|
||||
<config-description>
|
||||
<parameter name="username" type="text" required="true">
|
||||
<label>Username</label>
|
||||
<description></description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<label>Password</label>
|
||||
<context>password</context>
|
||||
<description></description>
|
||||
</parameter>
|
||||
<parameter name="url" type="text" required="true">
|
||||
<label>URL</label>
|
||||
<description> URL of the thermostat in the format 'proto://host' (example: https://192.168.1.100)</description>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer" min="1">
|
||||
<label>Refresh interval</label>
|
||||
<description>Specifies the refresh interval in seconds.</description>
|
||||
<default>30</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="systemMode">
|
||||
<item-type>String</item-type>
|
||||
<label>System Mode</label>
|
||||
<description>Current System Operating Mode</description>
|
||||
<state readOnly="false">
|
||||
<options>
|
||||
<option value="off">Off</option>
|
||||
<option value="heat">Heat</option>
|
||||
<option value="cool">Cool</option>
|
||||
<option value="auto">Auto</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="systemModeRaw" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>System Mode (Raw)</label>
|
||||
<description>Current System Operating Mode, as an integer number</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="systemState">
|
||||
<item-type>String</item-type>
|
||||
<label>System State</label>
|
||||
<description>Current System Operating State</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="idle">Idle</option>
|
||||
<option value="heating">Heating</option>
|
||||
<option value="cooling">Cooling</option>
|
||||
<option value="lockout">Lockout</option>
|
||||
<option value="error">Error</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="systemStateRaw" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>System State (Raw)</label>
|
||||
<description>Current System Operating State, as an integer</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="heatingSetpoint">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Heating Setpoint</label>
|
||||
<description>Heating Setpoint</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="false" pattern="%.1f %unit%" min="40" max="80" step="1.0"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="coolingSetpoint">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Cooling Setpoint</label>
|
||||
<description>Cooling Setpoint</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="false" pattern="%.1f %unit%" min="60" max="95" step="1.0"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%">
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="outdoorTemperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Outdoor Temperature</label>
|
||||
<description>Outdoor Temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%">
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="humidity">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Humidity</label>
|
||||
<description>Indoor Humidity</description>
|
||||
<category>Humidity</category>
|
||||
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user