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,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.radiothermostat</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,203 @@
# RadioThermostat Binding
This binding connects RadioThermostat/3M Filtrete models CT30, CT50/3M50, CT80, etc. with built-in Wi-Fi module to openHAB.
The binding retrieves and periodically updates all basic system information from the thermostat.
The main thermostat functions such as thermostat mode, fan mode, temperature set point and hold mode can be controlled.
System run-time information and humidity readings are polled less frequently and can be disabled completely if not desired.
Humidity information is available only when using a CT80 thermostat and I have noticed that the humidity reported is very inaccurate.
The main caveat for using this binding is to keep in mind that the web server in the thermostat is very slow.
Do not over load it with excessive amounts of simultaneous commands.
When changing the thermostat mode, the current temperature set point is cleared and a refresh of the thermostat data is done to get the new mode's set point.
Since retrieving the thermostat's data is the slowest operation, it will take several seconds after changing the mode before the new set point is displayed.
The 'Program Mode' command is untested and according to the published API is only available on a CT80 Rev B.
## Supported Things
There is exactly one supported thing type, which represents the thermostat.
It has the `rtherm` id.
Multiple Things can be added if more than one thermostat is to be controlled.
## Discovery
Auto-discovery is supported if the thermostat can be located on the local network using SSDP.
Otherwise the thing must be manually added.
## Binding Configuration
The binding has no configuration options, all configuration is done at Thing level.
## Thing Configuration
The thing has a few configuration parameters:
| Parameter | Description |
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| hostName | The host name or IP address of the thermostat. Mandatory. |
| refresh | Overrides the refresh interval of the thermostat data. Optional, the default is 2 minutes. |
| logRefresh | Overrides the refresh interval of the run-time logs & humidity data. Optional, the default is 10 minutes. |
| isCT80 | Flag to enable additional features only available on the CT80 thermostat. Optional, the default is false. |
| disableLogs | Disable retrieval of run-time logs from the thermostat. Optional, the default is false. |
| setpointMode | Controls temporary or absolute setpoint mode. In "temporary" mode the thermostat will temporarily maintain the given setpoint, returning to its program after a time. In "absolute" mode the thermostat will ignore its program maintaining the given setpoint. |
## Channels
The thermostat information that is retrieved is available as these channels:
| Channel ID | Item Type | Description |
|------------------------|----------------------|---------------------------------------------------------------------------|
| temperature | Number:Temperature | The current temperature reading of the thermostat |
| humidity | Number:Dimensionless | The current humidity reading of the thermostat (CT80 only) |
| mode | Number | The current operating mode of the HVAC system |
| fan_mode | Number | The current operating mode of the fan |
| program_mode | Number | The program schedule that the thermostat is running (CT80 Rev B only) |
| set_point | Number:Temperature | The current temperature set point of the thermostat |
| status | Number | Indicates the current running status of the HVAC system |
| fan_status | Number | Indicates the current fan status of the HVAC system |
| override | Number | Indicates if the normal program set-point has been manually overridden |
| hold | Switch | Indicates if the current set point temperature is to be held indefinitely |
| day | Number | The current day of the week reported by the thermostat (0 = Monday) |
| hour | Number | The current hour of the day reported by the thermostat (24 hr) |
| minute | Number | The current minute past the hour reported by the thermostat |
| dt_stamp | String | The current day of the week and time reported by the thermostat (E HH:mm) |
| today_heat_runtime | Number:Time | The total number of minutes of heating run-time today |
| today_cool_runtime | Number:Time | The total number of minutes of cooling run-time today |
| yesterday_heat_runtime | Number:Time | The total number of minutes of heating run-time yesterday |
| yesterday_cool_runtime | Number:Time | The total number of minutes of cooling run-time yesterday |
## Full Example
radiotherm.map:
```text
UNDEF_stus=-
NULL_stus=-
-_stus=-
0_stus=Off
1_stus=Heating
2_stus=Cooling
UNDEF_fstus=-
NULL_fstus=-
-_fstus=-
0_fstus=Off
1_fstus=On
UNDEF_mode=-
NULL_mode=-
-_mode=-
0_mode=Off
1_mode=Heat
2_mode=Cool
3_mode=Auto
UNDEF_fan=-
NULL_fan=-
-_fan=-
0_fan=Auto
1_fan=Auto/Circulate
2_fan=On
UNDEF_pgm=-
NULL_pgm=-
-_pgm=-
-1_pgm=None
0_pgm=Program A
1_pgm=Program B
2_pgm=Vacation
3_pgm=Holiday
UNDEF_over=-
NULL_over=-
-_over=-
0_over=No
1_over=Yes
```
radiotherm.things:
```java
radiothermostat:rtherm:mytherm1 "My 1st floor thermostat" [ hostName="192.168.10.1", refresh=2, logRefresh=10, isCT80=false, disableLogs=false, setpointMode="temporary" ]
radiothermostat:rtherm:mytherm2 "My 2nd floor thermostat" [ hostName="mythermhost2", refresh=1, logRefresh=20, isCT80=true, disableLogs=false, setpointMode="absolute" ]
```
radiotherm.items:
```java
Number:Temperature Therm_Temp "Current Temperature [%.1f °F] " <temperature> { channel="radiothermostat:rtherm:mytherm1:temperature" }
// Humidity only supported on CT80
Number Therm_Hum "Current Humidity [%d %%]" <temperature> { channel="radiothermostat:rtherm:mytherm1:humidity" }
Number Therm_Mode "Thermostat Mode [MAP(radiotherm.map):%s_mode]" { channel="radiothermostat:rtherm:mytherm1:mode" }
// The Auto/Circulate option will only appear for CT80
Number Therm_Fmode "Fan Mode [MAP(radiotherm.map):%s_fan]" { channel="radiothermostat:rtherm:mytherm1:fan_mode" }
// Program Mode only supported on CT80 Rev B
Number Therm_Pmode "Program Mode [MAP(radiotherm.map):%s_pgm]" { channel="radiothermostat:rtherm:mytherm1:program_mode" }
Number:Temperature Therm_Setpt "Set Point [%d]" <temperature> { channel="radiothermostat:rtherm:mytherm1:set_point" }
Number Therm_Status "Status [MAP(radiotherm.map):%s_stus]" { channel="radiothermostat:rtherm:mytherm1:status" }
Number Therm_FanStatus "Fan Status [MAP(radiotherm.map):%s_fstus]" { channel="radiothermostat:rtherm:mytherm1:fan_status" }
Number Therm_Override "Override [MAP(radiotherm.map):%s_over]" { channel="radiothermostat:rtherm:mytherm1:override" }
Switch Therm_Hold "Hold" { channel="radiothermostat:rtherm:mytherm1:hold" }
Number Therm_Day "Thermostat Day [%s]" { channel="radiothermostat:rtherm:mytherm1:day" }
Number Therm_Hour "Thermostat Hour [%s]" { channel="radiothermostat:rtherm:mytherm1:hour" }
Number Therm_Minute "Thermostat Minute [%s]" { channel="radiothermostat:rtherm:mytherm1:minute" }
String Therm_Dstmp "Thermostat DateStamp [%s]" <time> { channel="radiothermostat:rtherm:mytherm1:dt_stamp" }
Number:Time Therm_todayheat "Today's Heating Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:today_heat_runtime" }
Number:Time Therm_todaycool "Today's Cooling Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:today_cool_runtime" }
Number:Time Therm_yesterdayheat "Yesterday's Heating Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:yesterday_heat_runtime" }
Number:Time Therm_yesterdaycool "Yesterday's Cooling Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:yesterday_cool_runtime" }
// A virtual switch used to trigger a rule to send a json command to the thermostat
Switch Therm_mysetting "Send my preferred setting"
```
radiotherm.sitemap:
```perl
sitemap radiotherm label="My Thermostat" {
Frame label="My 1st floor thermostat" {
Text item=Therm_Temp icon="temperature" valuecolor=[>76="orange",>67.5="green",<=67.5="blue"]
// Humidity only supported on CT80
Text item=Therm_Hum icon="humidity"
Setpoint item=Therm_Setpt label="Target temperature [%d °F]" visibility=[Therm_Mode==1,Therm_Mode==2] icon="temperature" minValue=60 maxValue=85 step=1
Selection item=Therm_Mode icon="climate"
Selection item=Therm_Fmode icon="fan"
// Program Mode only supported on CT80 Rev B
Selection item=Therm_Pmode icon="smoke"
Text item=Therm_Status icon="climate"
Text item=Therm_FanStatus icon="flow"
Text item=Therm_Override icon="smoke"
Switch item=Therm_Hold icon="smoke"
// Virtual switch/button to trigger a rule to send a custom command
// The ON value displays in the button
Switch item=Therm_mysetting mappings=[ON="Heat, 58, hold"]
Text item=Therm_Day
Text item=Therm_Hour
Text item=Therm_Minute
Text item=Therm_Dstmp
Text item=Therm_todayheat
Text item=Therm_todaycool
Text item=Therm_yesterdayheat
Text item=Therm_yesterdaycool
}
}
```
radiotherm.rules:
```java
rule "Send my thermostat command"
when
Item Therm_mysetting received command
then
val actions = getActions("radiothermostat","radiothermostat:rtherm:mytherm1")
if(null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
}
// JSON to send directly to the thermostat's '/tstat' endpoint
// See RadioThermostat_CT50_Honeywell_Wifi_API_V1.3.pdf for more detail
actions.sendRawCommand('{"hold":1, "t_heat":' + "58" + ', "tmode":1}')
end
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.radiothermostat</artifactId>
<name>openHAB Add-ons :: Bundles :: RadioThermostat Binding</name>
</project>

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>