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.radiothermostat-${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-radiothermostat" description="Radio Thermostat Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.radiothermostat/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,28 @@
/**
* 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.radiothermostat.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link IRadioThermostatThingActions} defines the interface for all thing actions supported by the binding.
* These methods, parameters, and return types are explained in {@link RadioThermostatThingActions}.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public interface IRadioThermostatThingActions {
void sendRawCommand(@Nullable String rawCommand);
}

View File

@@ -0,0 +1,84 @@
/**
* 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.radiothermostat.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Temperature;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link RadioThermostatBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RadioThermostatBindingConstants {
public static final String BINDING_ID = "radiothermostat";
public static final String LOCAL = "local";
public static final String PROPERTY_IP = "hostName";
public static final String PROPERTY_ISCT80 = "isCT80";
public static final String KEY_ERROR = "error";
// List of JSON resources
public static final String DEFAULT_RESOURCE = "tstat";
public static final String RUNTIME_RESOURCE = "tstat/datalog";
public static final String HUMIDITY_RESOURCE = "tstat/humidity";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_RTHERM = new ThingTypeUID(BINDING_ID, "rtherm");
// List of all Channel id's
public static final String TEMPERATURE = "temperature";
public static final String HUMIDITY = "humidity";
public static final String MODE = "mode";
public static final String FAN_MODE = "fan_mode";
public static final String PROGRAM_MODE = "program_mode";
public static final String SET_POINT = "set_point";
public static final String OVERRIDE = "override";
public static final String HOLD = "hold";
public static final String STATUS = "status";
public static final String FAN_STATUS = "fan_status";
public static final String DAY = "day";
public static final String HOUR = "hour";
public static final String MINUTE = "minute";
public static final String DATE_STAMP = "dt_stamp";
public static final String TODAY_HEAT_RUNTIME = "today_heat_runtime";
public static final String TODAY_COOL_RUNTIME = "today_cool_runtime";
public static final String YESTERDAY_HEAT_RUNTIME = "yesterday_heat_runtime";
public static final String YESTERDAY_COOL_RUNTIME = "yesterday_cool_runtime";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_RTHERM);
public static final Set<String> SUPPORTED_CHANNEL_IDS = Stream.of(TEMPERATURE, HUMIDITY, MODE, FAN_MODE,
PROGRAM_MODE, SET_POINT, OVERRIDE, HOLD, STATUS, FAN_STATUS, DAY, HOUR, MINUTE, DATE_STAMP,
TODAY_HEAT_RUNTIME, TODAY_COOL_RUNTIME, YESTERDAY_HEAT_RUNTIME, YESTERDAY_COOL_RUNTIME)
.collect(Collectors.toSet());
// Units of measurement of the data delivered by the API
public static final Unit<Temperature> API_TEMPERATURE_UNIT = ImperialUnits.FAHRENHEIT;
public static final Unit<Dimensionless> API_HUMIDITY_UNIT = SmartHomeUnits.PERCENT;
public static final Unit<Time> API_MINUTES_UNIT = SmartHomeUnits.MINUTE;
}

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.radiothermostat.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link RadioThermostatConfiguration} is the class used to match the
* thing configuration.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RadioThermostatConfiguration {
public @Nullable String hostName;
public @Nullable Integer refresh;
public @Nullable Integer logRefresh;
public boolean isCT80 = false;
public boolean disableLogs = false;
public String setpointMode = "temporary";
}

View File

@@ -0,0 +1,71 @@
/**
* 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.radiothermostat.internal;
import static org.openhab.binding.radiothermostat.internal.RadioThermostatBindingConstants.THING_TYPE_RTHERM;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.radiothermostat.internal.handler.RadioThermostatHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link RadioThermostatHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.radiothermostat")
public class RadioThermostatHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_RTHERM);
private final RadioThermostatStateDescriptionProvider stateDescriptionProvider;
private final HttpClient httpClient;
@Activate
public RadioThermostatHandlerFactory(final @Reference RadioThermostatStateDescriptionProvider provider,
final @Reference HttpClientFactory httpClientFactory) {
this.stateDescriptionProvider = provider;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@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_RTHERM)) {
RadioThermostatHandler handler = new RadioThermostatHandler(thing, stateDescriptionProvider, httpClient);
return handler;
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.radiothermostat.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link RadioThermostatHttpException} extends Exception
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RadioThermostatHttpException extends Exception {
private static final long serialVersionUID = 1L;
public RadioThermostatHttpException(String errorMessage) {
super(errorMessage);
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.radiothermostat.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link RadioThermostatStateDescriptionProvider} class is a dynamic provider of state options while leaving other
* state description fields as original.
*
* @author Gregory Moyer - Initial contribution
* @author Michael Lobstein - Adapted for RadioThermostat Binding
*/
@Component(service = { DynamicStateDescriptionProvider.class, RadioThermostatStateDescriptionProvider.class })
@NonNullByDefault
public class RadioThermostatStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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.radiothermostat.internal;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.radiothermostat.internal.handler.RadioThermostatHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Some automation actions to be used with a {@link RadioThermostatThingActions}
*
* @author Michael Lobstein - initial contribution
*
*/
@ThingActionsScope(name = "radiothermostat")
@NonNullByDefault
public class RadioThermostatThingActions implements ThingActions, IRadioThermostatThingActions {
private final Logger logger = LoggerFactory.getLogger(RadioThermostatThingActions.class);
private @Nullable RadioThermostatHandler handler;
@Override
@RuleAction(label = "sendRawCommand", description = "Action that sends raw command to the thermostat")
public void sendRawCommand(@ActionInput(name = "sendRawCommand") @Nullable String rawCommand) {
RadioThermostatHandler localHandler = handler;
if (rawCommand == null) {
logger.warn("sendRawCommand called with null command, ignoring");
return;
}
if (localHandler != null) {
localHandler.handleRawCommand(rawCommand);
logger.debug("sendRawCommand called with raw command: {}", rawCommand);
}
}
/** Static alias to support the old DSL rules engine and make the action available there. */
public static void sendRawCommand(@Nullable ThingActions actions, @Nullable String rawCommand)
throws IllegalArgumentException {
invokeMethodOf(actions).sendRawCommand(rawCommand);
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (RadioThermostatHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
private static IRadioThermostatThingActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(RadioThermostatThingActions.class.getName())) {
if (actions instanceof RadioThermostatThingActions) {
return (IRadioThermostatThingActions) actions;
} else {
return (IRadioThermostatThingActions) Proxy.newProxyInstance(
IRadioThermostatThingActions.class.getClassLoader(),
new Class[] { IRadioThermostatThingActions.class },
(Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of RadioThermostatThingActions");
}
}

View File

@@ -0,0 +1,173 @@
/**
* 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.radiothermostat.internal.communication;
import static org.eclipse.jetty.http.HttpMethod.GET;
import static org.eclipse.jetty.http.HttpStatus.OK_200;
import static org.openhab.binding.radiothermostat.internal.RadioThermostatBindingConstants.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
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.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.openhab.binding.radiothermostat.internal.RadioThermostatHttpException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class for communicating with the RadioThermostat web interface
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RadioThermostatConnector {
private final Logger logger = LoggerFactory.getLogger(RadioThermostatConnector.class);
private static final String URL = "http://%hostName%/%resource%";
private final HttpClient httpClient;
private final List<RadioThermostatEventListener> listeners = new CopyOnWriteArrayList<>();
private @Nullable String hostName;
public RadioThermostatConnector(HttpClient httpClient) {
this.httpClient = httpClient;
}
public void setThermostatHostName(@Nullable String hostName) {
this.hostName = hostName;
}
/**
* Add a listener to the list of listeners to be notified with events
*
* @param listener the listener
*/
public void addEventListener(RadioThermostatEventListener listener) {
listeners.add(listener);
}
/**
* Remove a listener from the list of listeners to be notified with events
*
* @param listener the listener
*/
public void removeEventListener(RadioThermostatEventListener listener) {
listeners.remove(listener);
}
/**
* Send an asynchronous http call to the thermostat, the response will be send to the
* event listeners as a RadioThermostat event when it is finally received
*
* @param resouce the url of the json resource on the thermostat
*/
public void getAsyncThermostatData(String resource) {
String urlStr = buildRequestURL(resource);
httpClient.newRequest(urlStr).method(GET).timeout(20, TimeUnit.SECONDS).send(new BufferingResponseListener() {
@Override
public void onComplete(@Nullable Result result) {
if (result != null && !result.isFailed()) {
String response = getContentAsString();
logger.debug("thermostatResponse = {}", response);
dispatchKeyValue(resource, response);
} else {
dispatchKeyValue(KEY_ERROR, "");
}
}
});
}
/**
* Sends a command to the thermostat
*
* @param the JSON attribute key for the value to be updated
* @param the value to be updated in the thermostat
* @return the JSON response string from the thermostat
*/
public String sendCommand(String cmdKey, @Nullable String cmdVal) {
return sendCommand(cmdKey, cmdVal, null);
}
/**
* Sends a command to the thermostat
*
* @param the JSON attribute key for the value to be updated
* @param the value to be updated in the thermostat
* @param JSON string to send directly to the thermostat instead of a key/value pair
* @return the JSON response string from the thermostat
*/
public String sendCommand(@Nullable String cmdKey, @Nullable String cmdVal, @Nullable String cmdJson) {
// if we got a cmdJson string send that, otherwise build the json from the key and val params
String postJson = cmdJson != null ? cmdJson : "{\"" + cmdKey + "\":" + cmdVal + "}";
String urlStr = buildRequestURL(DEFAULT_RESOURCE);
String output = "";
try {
Request request = httpClient.POST(urlStr);
request.header(HttpHeader.ACCEPT, "text/plain");
request.header(HttpHeader.CONTENT_TYPE, "text/plain");
request.content(new StringContentProvider(postJson), "application/json");
ContentResponse contentResponse = request.send();
int httpStatus = contentResponse.getStatus();
if (httpStatus != OK_200) {
throw new RadioThermostatHttpException("Thermostat HTTP response code was: " + httpStatus);
}
output = contentResponse.getContentAsString();
} catch (RadioThermostatHttpException | InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("Error executing thermostat command: {}, {}", postJson, e.getMessage());
}
return output;
}
/**
* Build request URL from configuration data
*
* @return a valid URL for the thermostat's JSON interface
*/
private String buildRequestURL(String resource) {
String urlStr = URL.replace("%hostName%", hostName);
urlStr = urlStr.replace("%resource%", resource);
return urlStr;
}
/**
* Dispatch an event (key, value) to the event listeners
*
* @param key the key
* @param value the value
*/
private void dispatchKeyValue(String key, String value) {
RadioThermostatEvent event = new RadioThermostatEvent(this, key, value);
for (RadioThermostatEventListener listener : listeners) {
listener.onNewMessageEvent(event);
}
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.radiothermostat.internal.communication;
import java.util.EventObject;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* RadioThermostatEvent used to pass json update data received from the thermostat
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RadioThermostatEvent extends EventObject {
private static final long serialVersionUID = 1L;
private String key;
private String value;
public RadioThermostatEvent(Object source, String key, String value) {
super(source);
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.radiothermostat.internal.communication;
import java.util.EventListener;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* RadtioThermostat Event Listener interface. Handles incoming RadioThermostat message events
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public interface RadioThermostatEventListener extends EventListener {
/**
* Event handler method for incoming RadioThermostat message events
*
* @param event the event object
*/
public void onNewMessageEvent(RadioThermostatEvent event);
}

View File

@@ -0,0 +1,284 @@
/**
* 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.radiothermostat.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.MalformedURLException;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Scanner;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.radiothermostat.internal.RadioThermostatBindingConstants;
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.io.net.http.HttpUtil;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
/**
* The {@link RadioThermostatDiscoveryService} is responsible for discovery of
* RadioThermostats on the local network
*
* @author William Welliver - Initial contribution
* @author Dan Cunningham - Refactoring and Improvements
* @author Bill Forsyth - Modified for the RadioThermostat's peculiar discovery mode
* @author Michael Lobstein - Cleanup for RadioThermostat
*
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.radiothermostat")
public class RadioThermostatDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(RadioThermostatDiscoveryService.class);
private static final String RADIOTHERMOSTAT_DISCOVERY_MESSAGE = "TYPE: WM-DISCOVER\r\nVERSION: 1.0\r\n\r\nservices:com.marvell.wm.system*\r\n\r\n";
private static final String SSDP_MATCH = "WM-NOTIFY";
private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
private @Nullable ScheduledFuture<?> scheduledFuture = null;
public RadioThermostatDiscoveryService() {
super(RadioThermostatBindingConstants.SUPPORTED_THING_TYPES_UIDS, 30, true);
}
@Override
protected void startBackgroundDiscovery() {
logger.debug("Starting Background Scan");
stopBackgroundDiscovery();
scheduledFuture = scheduler.scheduleWithFixedDelay(this::doRunRun, 0, BACKGROUND_SCAN_INTERVAL_SECONDS,
TimeUnit.SECONDS);
}
@SuppressWarnings("null")
@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.debug("Sending SSDP discover.");
try {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
while (nets.hasMoreElements()) {
NetworkInterface ni = nets.nextElement();
if (ni.isUp() && ni.supportsMulticast() && !ni.isLoopback()) {
sendDiscoveryBroacast(ni);
}
}
} catch (IOException e) {
logger.debug("Error discovering 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 void sendDiscoveryBroacast(NetworkInterface ni)
throws UnknownHostException, SocketException, UnsupportedEncodingException {
InetAddress m = InetAddress.getByName("239.255.255.250");
final int port = 1900;
logger.debug("Sending discovery broadcast");
try {
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.debug("no ipv4 address on {}", ni.getName());
return;
}
// for whatever reason, the radio 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(null);
socket.setSoTimeout(5000);
socket.setReuseAddress(true);
// socket.setBroadcast(true);
socket.setNetworkInterface(ni);
socket.joinGroup(m);
logger.debug("Joined UPnP Multicast group on Interface: {}", ni.getName());
byte[] requestMessage = RADIOTHERMOSTAT_DISCOVERY_MESSAGE.getBytes("UTF-8");
DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, m, port);
socket.send(datagramPacket);
try {
// Try to ensure that joinGroup has taken effect. Without this delay, the query
// packet ends up going out before the group join.
Thread.sleep(1000);
socket.send(datagramPacket);
byte[] buf = new byte[4096];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
try {
while (!Thread.interrupted()) {
socket.receive(packet);
String response = new String(packet.getData(), StandardCharsets.UTF_8);
logger.debug("Response: {} ", response);
if (response.contains(SSDP_MATCH)) {
logger.debug("Match: {} ", response);
parseResponse(response);
}
}
logger.debug("Bridge device scan interrupted");
} catch (SocketTimeoutException e) {
logger.debug(
"Timed out waiting for multicast response. Presumably all devices have already responded.");
}
} finally {
socket.leaveGroup(m);
socket.close();
}
} catch (IOException | InterruptedException e) {
logger.debug("got exception: {}", e.getMessage());
}
return;
}
/**
* 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
*/
protected void parseResponse(String response) {
DiscoveryResult result;
String name = "unknownName";
String uuid = "unknownThermostat";
String ip = null;
String url = 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.debug("key: {} value: {}.", key, value);
if ("location".equals(key)) {
try {
url = value;
ip = new URL(value).getHost();
} catch (MalformedURLException e) {
logger.debug("Malfored URL {}", e.getMessage());
}
}
}
scanner.close();
logger.debug("Found thermostat, ip: {} ", ip);
if (ip == null) {
logger.debug("Bad Format from thermostat");
return;
}
JsonObject content;
String sysinfo;
boolean isCT80 = false;
try {
// Run the HTTP request and get the JSON response from the thermostat
sysinfo = HttpUtil.executeUrl("GET", url, 20000);
content = new JsonParser().parse(sysinfo).getAsJsonObject();
uuid = content.get("uuid").getAsString();
} catch (IOException | JsonSyntaxException e) {
logger.debug("Cannot get system info from thermostat {} {}", ip, e.getMessage());
sysinfo = null;
}
try {
String nameinfo = HttpUtil.executeUrl("GET", url + "name", 20000);
content = new JsonParser().parse(nameinfo).getAsJsonObject();
name = content.get("name").getAsString();
} catch (IOException | JsonSyntaxException e) {
logger.debug("Cannot get name from thermostat {} {}", ip, e.getMessage());
}
try {
String model = HttpUtil.executeUrl("GET", "http://" + ip + "/tstat/model", 20000);
isCT80 = (model != null && model.contains("CT80")) ? true : false;
} catch (IOException | JsonSyntaxException e) {
logger.debug("Cannot get model information from thermostat {} {}", ip, e.getMessage());
}
logger.debug("Discovery returned: {} uuid {} name {}", sysinfo, uuid, name);
ThingUID thingUid = new ThingUID(RadioThermostatBindingConstants.THING_TYPE_RTHERM, uuid);
logger.debug("Got discovered device.");
String label = String.format("RadioThermostat (%s)", name);
result = DiscoveryResultBuilder.create(thingUid).withLabel(label)
.withRepresentationProperty(RadioThermostatBindingConstants.PROPERTY_IP)
.withProperty(RadioThermostatBindingConstants.PROPERTY_IP, ip)
.withProperty(RadioThermostatBindingConstants.PROPERTY_ISCT80, isCT80).build();
logger.debug("New RadioThermostat discovered with ID=<{}>", uuid);
this.thingDiscovered(result);
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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.radiothermostat.internal.dto;
/**
* The {@link RadioThermostatDTO} is responsible for storing
* all of the JSON data objects that are retrieved from the thermostat
*
* @author Michael Lobstein - Initial contribution
*/
public class RadioThermostatDTO {
private RadioThermostatTstatDTO thermostatData;
private Integer humidity;
private RadioThermostatRuntimeDTO runtime;
public RadioThermostatDTO() {
}
public RadioThermostatTstatDTO getThermostatData() {
return thermostatData;
}
public void setThermostatData(RadioThermostatTstatDTO thermostatData) {
this.thermostatData = thermostatData;
}
public Integer getHumidity() {
return humidity;
}
public void setHumidity(Integer humidity) {
this.humidity = humidity;
}
public RadioThermostatRuntimeDTO getRuntime() {
return runtime;
}
public void setRuntime(RadioThermostatRuntimeDTO runtime) {
this.runtime = runtime;
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.radiothermostat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link RadioThermostatHumidityDTO} is responsible for storing
* the data from the thermostat 'tstat/humidity' JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class RadioThermostatHumidityDTO {
@SerializedName("humidity")
private Integer humidity;
public RadioThermostatHumidityDTO() {
}
public Integer getHumidity() {
return humidity;
}
}

View File

@@ -0,0 +1,51 @@
/**
* 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.radiothermostat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link RadioThermostatRuntimeDTO} is responsible for storing
* the "today" and "yesterday" node from the "tstat/datalog" JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class RadioThermostatRuntimeDTO {
@SerializedName("today")
private RadioThermostatRuntimeHeatCoolDTO today;
@SerializedName("yesterday")
private RadioThermostatRuntimeHeatCoolDTO yesterday;
public RadioThermostatRuntimeDTO() {
}
/**
* Receives "today" node from the JSON response
*
* @return {RadioThermostatRuntimeHeatCool}
*/
public RadioThermostatRuntimeHeatCoolDTO getToday() {
return today;
}
/**
* Receives "yesterday" node from the JSON response
*
* @return {RadioThermostatRuntimeHeatCool}
*/
public RadioThermostatRuntimeHeatCoolDTO getYesterday() {
return yesterday;
}
}

View File

@@ -0,0 +1,51 @@
/**
* 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.radiothermostat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link RadioThermostatRuntimeHeatCoolDTO} is responsible for storing
* the "heat_runtime" and "cool_runtime" node from the thermostat JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class RadioThermostatRuntimeHeatCoolDTO {
public RadioThermostatRuntimeHeatCoolDTO() {
}
@SerializedName("heat_runtime")
private RadioThermostatTimeDTO heatTime;
@SerializedName("cool_runtime")
private RadioThermostatTimeDTO coolTime;
/**
* Receives "heat_runtime" node from the JSON response
*
* @return {RadioThermostatJsonTime}
*/
public RadioThermostatTimeDTO getHeatTime() {
return heatTime;
}
/**
* Receives "cool_runtime" node from the JSON response
*
* @return {RadioThermostatJsonTime}
*/
public RadioThermostatTimeDTO getCoolTime() {
return coolTime;
}
}

View File

@@ -0,0 +1,92 @@
/**
* 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.radiothermostat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link RadioThermostatTimeDTO} is responsible for storing
* the "time" node from the thermostat JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class RadioThermostatTimeDTO {
@SerializedName("day")
private Integer dayOfWeek;
@SerializedName("hour")
private Integer hour;
@SerializedName("minute")
private Integer minute;
public RadioThermostatTimeDTO() {
}
public Integer getDayOfWeek() {
return dayOfWeek;
}
public Integer getHour() {
return hour;
}
public Integer getMinute() {
return minute;
}
/**
* Convenience method to return the total number of runtime minutes
*
* @return {runtime hours + minutes as minutes Integer}
*/
public Integer getRuntime() {
return (hour * 60) + minute;
}
/**
* Get formatted thermostat date stamp
*
* @return {Day of week/Time string}
*/
public String getThemostatDateTime() {
String day;
switch (dayOfWeek.toString()) {
case "0":
day = "Monday ";
break;
case "1":
day = "Tuesday ";
break;
case "2":
day = "Wedensday ";
break;
case "3":
day = "Thursday ";
break;
case "4":
day = "Friday ";
break;
case "5":
day = "Saturday ";
break;
case "6":
day = "Sunday ";
break;
default:
day = "";
}
return day + hour + ":" + String.format("%02d", minute);
}
}

View File

@@ -0,0 +1,148 @@
/**
* 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.radiothermostat.internal.dto;
import com.google.gson.annotations.SerializedName;
/**
* The {@link RadioThermostatTstatDTO} is responsible for storing
* the data from the thermostat 'tstat' JSON response
*
* @author Michael Lobstein - Initial contribution
*/
public class RadioThermostatTstatDTO {
@SerializedName("temp")
private Double temperature;
@SerializedName("tmode")
private Integer mode;
@SerializedName("fmode")
private Integer fanMode;
@SerializedName("program_mode")
private Integer programMode;
@SerializedName("t_heat")
private Integer heatTarget;
@SerializedName("t_cool")
private Integer coolTarget;
@SerializedName("override")
private Integer override;
@SerializedName("hold")
private Integer hold;
@SerializedName("tstate")
private Integer status;
@SerializedName("fstate")
private Integer fanStatus;
@SerializedName("time")
private RadioThermostatTimeDTO time;
public RadioThermostatTstatDTO() {
}
public Double getTemperature() {
return temperature;
}
public Integer getMode() {
return mode;
}
public void setMode(Integer mode) {
this.mode = mode;
}
public Integer getFanMode() {
return fanMode;
}
public void setFanMode(Integer fanMode) {
this.fanMode = fanMode;
}
public Integer getProgramMode() {
return programMode;
}
public void setProgramMode(Integer programMode) {
this.programMode = programMode;
}
public Integer getHeatTarget() {
return heatTarget;
}
public void setHeatTarget(Integer heatTarget) {
this.heatTarget = heatTarget;
}
public Integer getCoolTarget() {
return coolTarget;
}
public void setCoolTarget(Integer coolTarget) {
this.coolTarget = coolTarget;
}
public Integer getOverride() {
return override;
}
public Integer getHold() {
return hold;
}
public void setHold(Integer hold) {
this.hold = hold;
}
public Integer getStatus() {
return status;
}
public Integer getFanStatus() {
return fanStatus;
}
/**
* Determine if we are in heat mode or cool mode and return that temp value
*
* @return {Integer}
*/
public Integer getSetpoint() {
if (mode == 1) {
return heatTarget;
} else if (mode == 2) {
return coolTarget;
} else {
return 0;
}
}
/**
* Receives "time" node from the JSON response
*
* @return {RadioThermostatJsonTime}
*/
public RadioThermostatTimeDTO getTime() {
return time;
}
}

View File

@@ -0,0 +1,473 @@
/**
* 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.radiothermostat.internal.handler;
import static org.openhab.binding.radiothermostat.internal.RadioThermostatBindingConstants.*;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.text.ParseException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.radiothermostat.internal.RadioThermostatConfiguration;
import org.openhab.binding.radiothermostat.internal.RadioThermostatStateDescriptionProvider;
import org.openhab.binding.radiothermostat.internal.RadioThermostatThingActions;
import org.openhab.binding.radiothermostat.internal.communication.RadioThermostatConnector;
import org.openhab.binding.radiothermostat.internal.communication.RadioThermostatEvent;
import org.openhab.binding.radiothermostat.internal.communication.RadioThermostatEventListener;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatHumidityDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatRuntimeDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatTstatDTO;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link RadioThermostatHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* Based on the 'airquality' binding by Kuba Wolanin
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RadioThermostatHandler extends BaseThingHandler implements RadioThermostatEventListener {
private static final int DEFAULT_REFRESH_PERIOD = 2;
private static final int DEFAULT_LOG_REFRESH_PERIOD = 10;
private final RadioThermostatStateDescriptionProvider stateDescriptionProvider;
private final Logger logger = LoggerFactory.getLogger(RadioThermostatHandler.class);
private final Gson gson;
private final RadioThermostatConnector connector;
private final RadioThermostatDTO rthermData = new RadioThermostatDTO();
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> logRefreshJob;
private int refreshPeriod = DEFAULT_REFRESH_PERIOD;
private int logRefreshPeriod = DEFAULT_LOG_REFRESH_PERIOD;
private boolean isCT80 = false;
private boolean disableLogs = false;
private String setpointCmdKeyPrefix = "t_";
public RadioThermostatHandler(Thing thing, RadioThermostatStateDescriptionProvider stateDescriptionProvider,
HttpClient httpClient) {
super(thing);
this.stateDescriptionProvider = stateDescriptionProvider;
gson = new Gson();
connector = new RadioThermostatConnector(httpClient);
}
@Override
public void initialize() {
logger.debug("Initializing RadioThermostat handler.");
RadioThermostatConfiguration config = getConfigAs(RadioThermostatConfiguration.class);
final String hostName = config.hostName;
final Integer refresh = config.refresh;
final Integer logRefresh = config.logRefresh;
this.isCT80 = config.isCT80;
this.disableLogs = config.disableLogs;
if (hostName == null || hostName.equals("")) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Thermostat Host Name must be specified");
return;
}
if (refresh != null) {
this.refreshPeriod = refresh;
}
if (logRefresh != null) {
this.logRefreshPeriod = logRefresh;
}
connector.setThermostatHostName(hostName);
connector.addEventListener(this);
// The setpoint mode is controlled by the name of setpoint attribute sent to the thermostat.
// Temporary mode uses setpoint names prefixed with "t_" while absolute mode uses "a_"
if (config.setpointMode.equals("absolute")) {
this.setpointCmdKeyPrefix = "a_";
}
// populate fan mode options based on thermostat model
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), FAN_MODE), getFanModeOptions());
// if we are not a CT-80, remove the humidity & program mode channel
if (!this.isCT80) {
List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
channels.removeIf(c -> (c.getUID().getId().equals(HUMIDITY)));
channels.removeIf(c -> (c.getUID().getId().equals(PROGRAM_MODE)));
updateThing(editThing().withChannels(channels).build());
}
startAutomaticRefresh();
if (!this.disableLogs || this.isCT80) {
startAutomaticLogRefresh();
}
updateStatus(ThingStatus.UNKNOWN);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(RadioThermostatThingActions.class);
}
/**
* Start the job to periodically update data from the thermostat
*/
private void startAutomaticRefresh() {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob == null || refreshJob.isCancelled()) {
Runnable runnable = () -> {
// send an async call to the thermostat to get the 'tstat' data
connector.getAsyncThermostatData(DEFAULT_RESOURCE);
};
refreshJob = null;
this.refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refreshPeriod, TimeUnit.MINUTES);
}
}
/**
* Start the job to periodically update humidity and runtime date from the thermostat
*/
private void startAutomaticLogRefresh() {
ScheduledFuture<?> logRefreshJob = this.logRefreshJob;
if (logRefreshJob == null || logRefreshJob.isCancelled()) {
Runnable runnable = () -> {
// Request humidity data from the thermostat if we are a CT80
if (this.isCT80) {
// send an async call to the thermostat to get the humidity data
connector.getAsyncThermostatData(HUMIDITY_RESOURCE);
}
if (!this.disableLogs) {
// send an async call to the thermostat to get the runtime data
connector.getAsyncThermostatData(RUNTIME_RESOURCE);
}
};
logRefreshJob = null;
this.logRefreshJob = scheduler.scheduleWithFixedDelay(runnable, 1, logRefreshPeriod, TimeUnit.MINUTES);
}
}
@Override
public void dispose() {
logger.debug("Disposing the RadioThermostat handler.");
connector.removeEventListener(this);
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob != null) {
refreshJob.cancel(true);
this.refreshJob = null;
}
ScheduledFuture<?> logRefreshJob = this.logRefreshJob;
if (logRefreshJob != null) {
logRefreshJob.cancel(true);
this.logRefreshJob = null;
}
}
public void handleRawCommand(@Nullable String rawCommand) {
connector.sendCommand(null, null, rawCommand);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
updateChannel(channelUID.getId(), rthermData);
} else {
Integer cmdInt = -1;
String cmdStr = command.toString();
if (cmdStr != null) {
try {
// parse out an Integer from the string
// ie '70.5 F' becomes 70, also handles negative numbers
cmdInt = NumberFormat.getInstance().parse(cmdStr).intValue();
} catch (ParseException e) {
logger.debug("Command: {} -> Not an integer", cmdStr);
}
}
switch (channelUID.getId()) {
case MODE:
// only do if commanded mode is different than current mode
if (!cmdInt.equals(rthermData.getThermostatData().getMode())) {
connector.sendCommand("tmode", cmdStr);
// set the new operating mode, reset everything else,
// because refreshing the tstat data below is really slow.
rthermData.getThermostatData().setMode(cmdInt);
rthermData.getThermostatData().setHeatTarget(0);
rthermData.getThermostatData().setCoolTarget(0);
updateChannel(SET_POINT, rthermData);
rthermData.getThermostatData().setHold(0);
updateChannel(HOLD, rthermData);
rthermData.getThermostatData().setProgramMode(-1);
updateChannel(PROGRAM_MODE, rthermData);
// now just trigger a refresh of the thermost to get the new active setpoint
// this takes a while for the JSON request to complete (async).
connector.getAsyncThermostatData(DEFAULT_RESOURCE);
}
break;
case FAN_MODE:
rthermData.getThermostatData().setFanMode(cmdInt);
connector.sendCommand("fmode", cmdStr);
break;
case PROGRAM_MODE:
rthermData.getThermostatData().setProgramMode(cmdInt);
connector.sendCommand("program_mode", cmdStr);
break;
case HOLD:
if (command instanceof OnOffType && command == OnOffType.ON) {
rthermData.getThermostatData().setHold(1);
connector.sendCommand("hold", "1");
} else if (command instanceof OnOffType && command == OnOffType.OFF) {
rthermData.getThermostatData().setHold(0);
connector.sendCommand("hold", "0");
}
break;
case SET_POINT:
String cmdKey = null;
if (rthermData.getThermostatData().getMode() == 1) {
cmdKey = this.setpointCmdKeyPrefix + "heat";
rthermData.getThermostatData().setHeatTarget(cmdInt);
} else if (rthermData.getThermostatData().getMode() == 2) {
cmdKey = this.setpointCmdKeyPrefix + "cool";
rthermData.getThermostatData().setCoolTarget(cmdInt);
} else {
// don't do anything if we are not in heat or cool mode
break;
}
connector.sendCommand(cmdKey, cmdInt.toString());
break;
default:
logger.warn("Unsupported command: {}", command.toString());
}
}
}
/**
* Handle a RadioThermostat event received from the listeners
*
* @param event the event received from the listeners
*/
@Override
public void onNewMessageEvent(RadioThermostatEvent event) {
logger.debug("onNewMessageEvent: key {} = {}", event.getKey(), event.getValue());
String evtKey = event.getKey();
String evtVal = event.getValue();
if (KEY_ERROR.equals(evtKey)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Error retrieving data from Thermostat ");
} else {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
// Map the JSON response to the correct object and update appropriate channels
switch (evtKey) {
case DEFAULT_RESOURCE:
rthermData.setThermostatData(gson.fromJson(evtVal, RadioThermostatTstatDTO.class));
updateAllChannels();
break;
case HUMIDITY_RESOURCE:
rthermData.setHumidity(gson.fromJson(evtVal, RadioThermostatHumidityDTO.class).getHumidity());
updateChannel(HUMIDITY, rthermData);
break;
case RUNTIME_RESOURCE:
rthermData.setRuntime(gson.fromJson(evtVal, RadioThermostatRuntimeDTO.class));
updateChannel(TODAY_HEAT_RUNTIME, rthermData);
updateChannel(TODAY_COOL_RUNTIME, rthermData);
updateChannel(YESTERDAY_HEAT_RUNTIME, rthermData);
updateChannel(YESTERDAY_COOL_RUNTIME, rthermData);
break;
}
}
}
/**
* Update the channel from the last Thermostat data retrieved
*
* @param channelId the id identifying the channel to be updated
*/
private void updateChannel(String channelId, RadioThermostatDTO rthermData) {
if (isLinked(channelId)) {
Object value;
try {
value = getValue(channelId, rthermData);
} catch (Exception e) {
logger.debug("Error setting {} value", channelId.toUpperCase());
return;
}
State state = null;
if (value == null) {
state = UnDefType.UNDEF;
} else if (value instanceof PointType) {
state = (PointType) value;
} else if (value instanceof ZonedDateTime) {
state = new DateTimeType((ZonedDateTime) value);
} else if (value instanceof QuantityType<?>) {
state = (QuantityType<?>) value;
} else if (value instanceof BigDecimal) {
state = new DecimalType((BigDecimal) value);
} else if (value instanceof Integer) {
state = new DecimalType(BigDecimal.valueOf(((Integer) value).longValue()));
} else if (value instanceof String) {
state = new StringType(value.toString());
} else if (value instanceof OnOffType) {
state = (OnOffType) value;
} else {
logger.warn("Update channel {}: Unsupported value type {}", channelId,
value.getClass().getSimpleName());
}
logger.debug("Update channel {} with state {} ({})", channelId, (state == null) ? "null" : state.toString(),
(value == null) ? "null" : value.getClass().getSimpleName());
// Update the channel
if (state != null) {
updateState(channelId, state);
}
}
}
/**
* Update a given channelId from the thermostat data
*
* @param the channel id to be updated
* @param data the RadioThermostat dto
* @return the value to be set in the state
*/
public static @Nullable Object getValue(String channelId, RadioThermostatDTO data) {
switch (channelId) {
case TEMPERATURE:
if (data.getThermostatData().getTemperature() != null) {
return new QuantityType<Temperature>(data.getThermostatData().getTemperature(),
API_TEMPERATURE_UNIT);
} else {
return null;
}
case HUMIDITY:
if (data.getHumidity() != null) {
return new QuantityType<>(data.getHumidity(), API_HUMIDITY_UNIT);
} else {
return null;
}
case MODE:
return data.getThermostatData().getMode();
case FAN_MODE:
return data.getThermostatData().getFanMode();
case PROGRAM_MODE:
return data.getThermostatData().getProgramMode();
case SET_POINT:
if (data.getThermostatData().getSetpoint() != 0) {
return new QuantityType<Temperature>(data.getThermostatData().getSetpoint(), API_TEMPERATURE_UNIT);
} else {
return null;
}
case OVERRIDE:
return data.getThermostatData().getOverride();
case HOLD:
return OnOffType.from(data.getThermostatData().getHold() == 1);
case STATUS:
return data.getThermostatData().getStatus();
case FAN_STATUS:
return data.getThermostatData().getFanStatus();
case DAY:
return data.getThermostatData().getTime().getDayOfWeek();
case HOUR:
return data.getThermostatData().getTime().getHour();
case MINUTE:
return data.getThermostatData().getTime().getMinute();
case DATE_STAMP:
return data.getThermostatData().getTime().getThemostatDateTime();
case TODAY_HEAT_RUNTIME:
return new QuantityType<>(data.getRuntime().getToday().getHeatTime().getRuntime(), API_MINUTES_UNIT);
case TODAY_COOL_RUNTIME:
return new QuantityType<>(data.getRuntime().getToday().getCoolTime().getRuntime(), API_MINUTES_UNIT);
case YESTERDAY_HEAT_RUNTIME:
return new QuantityType<>(data.getRuntime().getYesterday().getHeatTime().getRuntime(),
API_MINUTES_UNIT);
case YESTERDAY_COOL_RUNTIME:
return new QuantityType<>(data.getRuntime().getYesterday().getCoolTime().getRuntime(),
API_MINUTES_UNIT);
}
return null;
}
/**
* Updates all channels from rthermData
*/
private void updateAllChannels() {
// Update all channels from rthermData
for (Channel channel : getThing().getChannels()) {
updateChannel(channel.getUID().getId(), rthermData);
}
}
/**
* Build a list of fan modes based on what model thermostat is used
*
* @return list of state options for thermostat fan modes
*/
private List<StateOption> getFanModeOptions() {
List<StateOption> fanModeOptions = new ArrayList<>();
fanModeOptions.add(new StateOption("0", "Auto"));
if (this.isCT80) {
fanModeOptions.add(new StateOption("1", "Auto/Circulate"));
}
fanModeOptions.add(new StateOption("2", "On"));
return fanModeOptions;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="radiothermostat" 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>Radio Thermostat Binding</name>
<description>Controls the RadioThermostat model CT30, CT50 or CT80 via the built-in WIFI module</description>
<author>Michael Lobstein</author>
</binding:binding>

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="radiothermostat"
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">
<!-- RadioThemostat Thing -->
<thing-type id="rtherm">
<label>Thermostat</label>
<description>
A Thermostat to Control the House's HVAC System
</description>
<channels>
<channel id="temperature" typeId="temp-temperature"/>
<channel id="humidity" typeId="humidity"/>
<channel id="mode" typeId="mode"/>
<channel id="fan_mode" typeId="fan_mode"/>
<channel id="program_mode" typeId="program_mode"/>
<channel id="set_point" typeId="temp-sp"/>
<channel id="override" typeId="override"/>
<channel id="hold" typeId="hold"/>
<channel id="status" typeId="status"/>
<channel id="fan_status" typeId="fan_status"/>
<channel id="day" typeId="t_day"/>
<channel id="hour" typeId="t_hour"/>
<channel id="minute" typeId="t_minute"/>
<channel id="dt_stamp" typeId="dt_stamp"/>
<channel id="today_heat_runtime" typeId="today_heat_runtime"/>
<channel id="today_cool_runtime" typeId="today_cool_runtime"/>
<channel id="yesterday_heat_runtime" typeId="yesterday_heat_runtime"/>
<channel id="yesterday_cool_runtime" typeId="yesterday_cool_runtime"/>
</channels>
<config-description>
<parameter name="hostName" type="text" required="true">
<context>network-address</context>
<label>Thermostat Host Name/IP Address</label>
<description>Host Name or IP Address of the Thermostat</description>
</parameter>
<parameter name="refresh" type="integer" min="1" required="false" unit="min">
<label>Refresh Interval</label>
<description>Specifies the Refresh Interval in Minutes</description>
<default>2</default>
</parameter>
<parameter name="logRefresh" type="integer" min="5" required="false" unit="min">
<label>Run-time Log Refresh Interval</label>
<description>Specifies the Run-time Log and Humidity Refresh Interval in Minutes</description>
<default>10</default>
</parameter>
<parameter name="isCT80" type="boolean">
<label>Enable CT80 Thermostat Options</label>
<description>Optional Flag to Enable Additional Features Only Available on the CT80 Thermostat</description>
<default>false</default>
</parameter>
<parameter name="disableLogs" type="boolean">
<label>Disable Retrieval of Run-time Data</label>
<description>Optional Flag to Disable the Retrieval of Run-time Data from the Thermostat</description>
<default>false</default>
</parameter>
<parameter name="setpointMode" type="text">
<label>Setpoint Mode</label>
<description>Run in absolute or temporary setpoint mode</description>
<default>temporary</default>
<options>
<option value="absolute">Absolute</option>
<option value="temporary">Temporary</option>
</options>
</parameter>
</config-description>
</thing-type>
<channel-type id="temp-temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>The Current Temperature Reading of the Thermostat</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="humidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>The Current Humidity Reading of the Thermostat</description>
<category>Humidity</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="mode">
<item-type>Number</item-type>
<label>Mode</label>
<description>The Current Operating Mode of the HVAC System</description>
<state>
<options>
<option value="0">Off</option>
<option value="1">Heat</option>
<option value="2">Cool</option>
<option value="3">Auto</option>
</options>
</state>
</channel-type>
<channel-type id="fan_mode">
<item-type>Number</item-type>
<label>Fan Mode</label>
<description>The Current Operating Mode of the Fan</description>
</channel-type>
<channel-type id="program_mode" advanced="true">
<item-type>Number</item-type>
<label>Program Mode</label>
<description>The Program Schedule That the Thermostat Is Running</description>
<state>
<options>
<option value="-1">None</option>
<option value="0">Program A</option>
<option value="1">Program B</option>
<option value="2">Vacation</option>
<option value="3">Holiday</option>
</options>
</state>
</channel-type>
<channel-type id="temp-sp">
<item-type>Number</item-type>
<label>Setpoint</label>
<description>The Current Temperature Set Point of the Thermostat</description>
<category>Temperature</category>
<state min="35" max="95" pattern="%d"/>
</channel-type>
<channel-type id="override">
<item-type>Number</item-type>
<label>Override</label>
<description>Indicates If the Normal Program Setpoint Has Been Manually Overriden</description>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="hold">
<item-type>Switch</item-type>
<label>Hold</label>
<description>Indicates If the Current Set Point Temperature Is to Be Held Indefinitely</description>
</channel-type>
<channel-type id="status">
<item-type>Number</item-type>
<label>Status</label>
<description>Indicates the Current Running Status of the HVAC System</description>
<state min="0" max="2" pattern="%d"/>
</channel-type>
<channel-type id="fan_status">
<item-type>Number</item-type>
<label>Fan Status</label>
<description>Indicates the Current Fan Status of the HVAC System</description>
<state min="0" max="2" pattern="%d"/>
</channel-type>
<channel-type id="t_day" advanced="true">
<item-type>Number</item-type>
<label>Day</label>
<description>The Current Day of the Week Reported by the Thermostat</description>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="t_hour" advanced="true">
<item-type>Number</item-type>
<label>Hour</label>
<description>The Current Hour of the Day Reported by the Thermostat</description>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="t_minute" advanced="true">
<item-type>Number</item-type>
<label>Minute</label>
<description>The Current Minute Past the Hour Reported by the Thermostat</description>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="dt_stamp" advanced="true">
<item-type>String</item-type>
<label>Thermostat Date</label>
<description>The Current Day of the Week and Time Reported by the Thermostat</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="today_heat_runtime">
<item-type>Number:Time</item-type>
<label>Today's Heating Runtime</label>
<description>The Number of Minutes of Heating Run-time Today</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="today_cool_runtime">
<item-type>Number:Time</item-type>
<label>Today's Cooling Runtime</label>
<description>The Number of Minutes of Cooling Run-time Today</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="yesterday_heat_runtime">
<item-type>Number:Time</item-type>
<label>Yesterday's Heating Runtime</label>
<description>The Number of Minutes of Heating Run-time Yesterday</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="yesterday_cool_runtime">
<item-type>Number:Time</item-type>
<label>Yesterday's Cooling Runtime</label>
<description>The Number of Minutes of Cooling Run-time Yesterday</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
</thing:thing-descriptions>