added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -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>

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>