diff --git a/CODEOWNERS b/CODEOWNERS
index 13b48f0bd423b..9e56bb4d5b467 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -164,6 +164,7 @@
/bundles/org.openhab.binding.powermax/ @lolodomo
/bundles/org.openhab.binding.pulseaudio/ @peuter
/bundles/org.openhab.binding.pushbullet/ @hakan42
+/bundles/org.openhab.binding.radiothermostat/ @mlobstein
/bundles/org.openhab.binding.regoheatpump/ @crnjan
/bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
/bundles/org.openhab.binding.rme/ @kgoderis
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index e8eec5fe346bf..263cd1c063409 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -809,6 +809,11 @@
org.openhab.binding.pushbullet
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.radiothermostat
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.regoheatpump
diff --git a/bundles/org.openhab.binding.radiothermostat/.classpath b/bundles/org.openhab.binding.radiothermostat/.classpath
new file mode 100644
index 0000000000000..a5d95095ccaaf
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.radiothermostat/.project b/bundles/org.openhab.binding.radiothermostat/.project
new file mode 100644
index 0000000000000..6efb0cb2923e5
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.binding.radiothermostat
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.binding.radiothermostat/NOTICE b/bundles/org.openhab.binding.radiothermostat/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/NOTICE
@@ -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
diff --git a/bundles/org.openhab.binding.radiothermostat/README.md b/bundles/org.openhab.binding.radiothermostat/README.md
new file mode 100644
index 0000000000000..ab307b4cd4904
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/README.md
@@ -0,0 +1,202 @@
+# 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. |
+
+## 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 ]
+radiothermostat:rtherm:mytherm2 "My 2nd floor thermostat" [ hostName="mythermhost2", refresh=1, logRefresh=20, isCT80=true, disableLogs=false ]
+```
+
+radiotherm.items:
+
+```java
+Number:Temperature Therm_Temp "Current Temperature [%.1f °F] " { channel="radiothermostat:rtherm:mytherm1:temperature" }
+// Humidity only supported on CT80
+Number Therm_Hum "Current Humidity [%d %%]" { 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]" { 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]" { 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
+```
diff --git a/bundles/org.openhab.binding.radiothermostat/pom.xml b/bundles/org.openhab.binding.radiothermostat/pom.xml
new file mode 100644
index 0000000000000..a204fd487798d
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 2.5.7-SNAPSHOT
+
+
+ org.openhab.binding.radiothermostat
+
+ openHAB Add-ons :: Bundles :: RadioThermostat Binding
+
+
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/feature/feature.xml b/bundles/org.openhab.binding.radiothermostat/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..2f03756331232
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.radiothermostat/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatBindingConstants.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatBindingConstants.java
new file mode 100644
index 0000000000000..313ab4df7bba0
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatBindingConstants.java
@@ -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.eclipse.smarthome.core.library.unit.ImperialUnits;
+import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
+import org.eclipse.smarthome.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 SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_RTHERM);
+ public static final Set 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 API_TEMPERATURE_UNIT = ImperialUnits.FAHRENHEIT;
+ public static final Unit API_HUMIDITY_UNIT = SmartHomeUnits.PERCENT;
+ public static final Unit API_MINUTES_UNIT = SmartHomeUnits.MINUTE;
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatConfiguration.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatConfiguration.java
new file mode 100644
index 0000000000000..18b8c4232f178
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatConfiguration.java
@@ -0,0 +1,31 @@
+/**
+ * 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;
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatHandlerFactory.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatHandlerFactory.java
new file mode 100644
index 0000000000000..e880973b70c54
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatHandlerFactory.java
@@ -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.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
+import org.eclipse.smarthome.io.net.http.HttpClientFactory;
+import org.openhab.binding.radiothermostat.internal.handler.RadioThermostatHandler;
+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 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatHttpException.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatHttpException.java
new file mode 100644
index 0000000000000..e0ff5d3710278
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatHttpException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatStateDescriptionProvider.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatStateDescriptionProvider.java
new file mode 100644
index 0000000000000..b5a63acbac247
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatStateDescriptionProvider.java
@@ -0,0 +1,64 @@
+/**
+ * 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.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.Channel;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider;
+import org.eclipse.smarthome.core.types.StateDescription;
+import org.eclipse.smarthome.core.types.StateDescriptionFragmentBuilder;
+import org.eclipse.smarthome.core.types.StateOption;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+
+/**
+ * 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 implements DynamicStateDescriptionProvider {
+ private final Map> channelOptionsMap = new ConcurrentHashMap<>();
+
+ public void setStateOptions(ChannelUID channelUID, List options) {
+ channelOptionsMap.put(channelUID, options);
+ }
+
+ @Override
+ public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
+ @Nullable Locale locale) {
+ List options = channelOptionsMap.get(channel.getUID());
+ if (options == null) {
+ return null;
+ }
+
+ StateDescriptionFragmentBuilder builder = (original == null) ? StateDescriptionFragmentBuilder.create()
+ : StateDescriptionFragmentBuilder.create(original);
+ return builder.withOptions(options).build().toStateDescription();
+ }
+
+ @Deactivate
+ public void deactivate() {
+ channelOptionsMap.clear();
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatThingActions.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatThingActions.java
new file mode 100644
index 0000000000000..6e0a371fc4af6
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/RadioThermostatThingActions.java
@@ -0,0 +1,68 @@
+/**
+ * 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;
+import org.eclipse.smarthome.core.thing.binding.ThingActions;
+import org.eclipse.smarthome.core.thing.binding.ThingActionsScope;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.openhab.binding.radiothermostat.internal.handler.RadioThermostatHandler;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.RuleAction;
+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 {
+ private final Logger logger = LoggerFactory.getLogger(RadioThermostatThingActions.class);
+
+ private @Nullable RadioThermostatHandler handler;
+
+ @SuppressWarnings("null")
+ @RuleAction(label = "sendRawCommand", description = "Action that sends raw command to the thermostat")
+ public void sendRawCommand(@ActionInput(name = "sendRawCommand") @Nullable String rawCommand) {
+ if (handler != null && rawCommand != null) {
+ handler.handleRawCommand(rawCommand);
+ logger.debug("sendRawCommand called with raw command: {}", rawCommand);
+ } else {
+ logger.debug("sendRawCommand called with null command, ignoring");
+ }
+ }
+
+ public static void sendRawCommand(@Nullable ThingActions actions, @Nullable String rawCommand)
+ throws IllegalArgumentException {
+ if (actions instanceof RadioThermostatThingActions) {
+ ((RadioThermostatThingActions) actions).sendRawCommand(rawCommand);
+ } else {
+ throw new IllegalArgumentException("Instance is not an RadioThermostatThingActions class.");
+ }
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ this.handler = (RadioThermostatHandler) handler;
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return this.handler;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatConnector.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatConnector.java
new file mode 100644
index 0000000000000..e46b9b5247680
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatConnector.java
@@ -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 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);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatEvent.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatEvent.java
new file mode 100644
index 0000000000000..c72f6985b5a43
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatEvent.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatEventListener.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatEventListener.java
new file mode 100644
index 0000000000000..38e8d8e07d92b
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/communication/RadioThermostatEventListener.java
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/discovery/RadioThermostatDiscoveryService.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/discovery/RadioThermostatDiscoveryService.java
new file mode 100644
index 0000000000000..8300f8075aa7e
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/discovery/RadioThermostatDiscoveryService.java
@@ -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.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
+import org.eclipse.smarthome.config.discovery.DiscoveryService;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.eclipse.smarthome.io.net.http.HttpUtil;
+import org.openhab.binding.radiothermostat.internal.RadioThermostatBindingConstants;
+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 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 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatDTO.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatDTO.java
new file mode 100644
index 0000000000000..ade0ab3346fc6
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatDTO.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatHumidityDTO.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatHumidityDTO.java
new file mode 100644
index 0000000000000..03bbd084723d6
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatHumidityDTO.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatRuntimeDTO.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatRuntimeDTO.java
new file mode 100644
index 0000000000000..fc3968ffe2680
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatRuntimeDTO.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatRuntimeHeatCoolDTO.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatRuntimeHeatCoolDTO.java
new file mode 100644
index 0000000000000..76eebe1d5310b
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatRuntimeHeatCoolDTO.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatTimeDTO.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatTimeDTO.java
new file mode 100644
index 0000000000000..432410dd39a3c
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatTimeDTO.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatTstatDTO.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatTstatDTO.java
new file mode 100644
index 0000000000000..f695e24d0de5a
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/dto/RadioThermostatTstatDTO.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/handler/RadioThermostatHandler.java b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/handler/RadioThermostatHandler.java
new file mode 100644
index 0000000000000..dac91d9010e0e
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/java/org/openhab/binding/radiothermostat/internal/handler/RadioThermostatHandler.java
@@ -0,0 +1,443 @@
+/**
+ * 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.eclipse.smarthome.core.library.types.DateTimeType;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PointType;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.thing.Channel;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerService;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.RefreshType;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.core.types.StateOption;
+import org.eclipse.smarthome.core.types.UnDefType;
+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.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 @Nullable 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 @Nullable RadioThermostatConfiguration config;
+
+ public RadioThermostatHandler(Thing thing,
+ @Nullable RadioThermostatStateDescriptionProvider stateDescriptionProvider, HttpClient httpClient) {
+ super(thing);
+ this.stateDescriptionProvider = stateDescriptionProvider;
+ gson = new Gson();
+ connector = new RadioThermostatConnector(httpClient);
+ }
+
+ @SuppressWarnings("null")
+ @Override
+ public void initialize() {
+ logger.debug("Initializing RadioThermostat handler.");
+ this.config = getConfigAs(RadioThermostatConfiguration.class);
+ connector.setThermostatHostName(config.hostName);
+ connector.addEventListener(this);
+
+ // populate fan mode options based on thermostat model
+ List fanModeOptions = getFanModeOptions(config.isCT80);
+ stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), FAN_MODE), fanModeOptions);
+
+ // if we are not a CT-80, remove the humidity & program mode channel
+ if (!config.isCT80) {
+ List 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 (!config.disableLogs || config.isCT80) {
+ startAutomaticLogRefresh();
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Collections.singletonList(RadioThermostatThingActions.class);
+ }
+
+ /**
+ * Start the job to periodically update data from the thermostat
+ */
+ @SuppressWarnings("null")
+ private void startAutomaticRefresh() {
+ if (refreshJob == null || refreshJob.isCancelled()) {
+ Runnable runnable = () -> {
+ // send an async call to the thermostat to get the 'tstat' data
+ connector.getAsyncThermostatData(DEFAULT_RESOURCE);
+ };
+
+ int delay = (config.refresh != null) ? config.refresh.intValue() : DEFAULT_REFRESH_PERIOD;
+ refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, delay, TimeUnit.MINUTES);
+ }
+ }
+
+ /**
+ * Start the job to periodically update humidity and runtime date from the thermostat
+ */
+ @SuppressWarnings("null")
+ private void startAutomaticLogRefresh() {
+ if (logRefreshJob == null || logRefreshJob.isCancelled()) {
+ Runnable runnable = () -> {
+ // Request humidity data from the thermostat if we are a CT80
+ if (config.isCT80) {
+ // send an async call to the thermostat to get the humidity data
+ connector.getAsyncThermostatData(HUMIDITY_RESOURCE);
+ }
+
+ if (!config.disableLogs) {
+ // send an async call to the thermostat to get the runtime data
+ connector.getAsyncThermostatData(RUNTIME_RESOURCE);
+ }
+ };
+
+ int delay = ((config.logRefresh != null) ? config.logRefresh.intValue() : DEFAULT_LOG_REFRESH_PERIOD) * 60;
+ logRefreshJob = scheduler.scheduleWithFixedDelay(runnable, 30, delay, TimeUnit.SECONDS);
+ }
+ }
+
+ @SuppressWarnings("null")
+ @Override
+ public void dispose() {
+ logger.debug("Disposing the RadioThermostat handler.");
+ connector.removeEventListener(this);
+
+ if (refreshJob != null) {
+ refreshJob.cancel(true);
+ refreshJob = null;
+ }
+ if (logRefreshJob != null) {
+ logRefreshJob.cancel(true);
+ 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 = "t_heat";
+ rthermData.getThermostatData().setHeatTarget(cmdInt);
+ } else if (rthermData.getThermostatData().getMode() == 2) {
+ cmdKey = "t_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(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(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 getFanModeOptions(boolean isCT80) {
+ List fanModeOptions = new ArrayList<>();
+
+ fanModeOptions.add(new StateOption("0", "Auto"));
+ if (isCT80) {
+ fanModeOptions.add(new StateOption("1", "Auto/Circulate"));
+ }
+ fanModeOptions.add(new StateOption("2", "On"));
+
+ return fanModeOptions;
+ }
+}
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.radiothermostat/src/main/resources/ESH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..c30f1513fc1d2
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/resources/ESH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ Radio Thermostat Binding
+ Controls the RadioThermostat model CT30, CT50 or CT80 via the built-in WIFI module
+ Michael Lobstein
+
+
diff --git a/bundles/org.openhab.binding.radiothermostat/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.radiothermostat/src/main/resources/ESH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..ecdadebcec0ad
--- /dev/null
+++ b/bundles/org.openhab.binding.radiothermostat/src/main/resources/ESH-INF/thing/thing-types.xml
@@ -0,0 +1,207 @@
+
+
+
+
+
+ Thermostat
+
+ A Thermostat to Control the House's HVAC System
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ network-address
+ Thermostat Host Name/IP Address
+ Host Name or IP Address of the Thermostat
+
+
+ Refresh Interval
+ Specifies the Refresh Interval in Minutes
+ 2
+
+
+ Run-time Log Refresh Interval
+ Specifies the Run-time Log and Humidity Refresh Interval in Minutes
+ 10
+
+
+ Enable CT80 Thermostat Options
+ Optional Flag to Enable Additional Features Only Available on the CT80 Thermostat
+ false
+
+
+ Disable Retrieval of Run-time Data
+ Optional Flag to Disable the Retrieval of Run-time Data from the Thermostat
+ false
+
+
+
+
+
+
+ Number:Temperature
+ Temperature
+ The Current Temperature Reading of the Thermostat
+ Temperature
+
+
+
+
+ Number:Dimensionless
+ Humidity
+ The Current Humidity Reading of the Thermostat
+ Humidity
+
+
+
+
+ Number
+ Mode
+ The Current Operating Mode of the HVAC System
+
+
+ Off
+ Heat
+ Cool
+ Auto
+
+
+
+
+
+ Number
+ Fan Mode
+ The Current Operating Mode of the Fan
+
+
+
+ Number
+ Program Mode
+ The Program Schedule That the Thermostat Is Running
+
+
+ None
+ Program A
+ Program B
+ Vacation
+ Holiday
+
+
+
+
+
+ Number
+ Setpoint
+ The Current Temperature Set Point of the Thermostat
+ Temperature
+
+
+
+
+ Number
+ Override
+ Indicates If the Normal Program Setpoint Has Been Manually Overriden
+
+
+
+
+ Switch
+ Hold
+ Indicates If the Current Set Point Temperature Is to Be Held Indefinitely
+
+
+
+ Number
+ Status
+ Indicates the Current Running Status of the HVAC System
+
+
+
+
+ Number
+ Fan Status
+ Indicates the Current Fan Status of the HVAC System
+
+
+
+
+ Number
+ Day
+ The Current Day of the Week Reported by the Thermostat
+
+
+
+
+ Number
+ Hour
+ The Current Hour of the Day Reported by the Thermostat
+
+
+
+
+ Number
+ Minute
+ The Current Minute Past the Hour Reported by the Thermostat
+
+
+
+
+ String
+ Thermostat Date
+ The Current Day of the Week and Time Reported by the Thermostat
+
+
+
+
+ Number:Time
+ Today's Heating Runtime
+ The Number of Minutes of Heating Run-time Today
+
+
+
+
+ Number:Time
+ Today's Cooling Runtime
+ The Number of Minutes of Cooling Run-time Today
+
+
+
+
+ Number:Time
+ Yesterday's Heating Runtime
+ The Number of Minutes of Heating Run-time Yesterday
+
+
+
+
+ Number:Time
+ Yesterday's Cooling Runtime
+ The Number of Minutes of Cooling Run-time Yesterday
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index de8830d0a7978..f86c3cbabee64 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -198,6 +198,7 @@
org.openhab.binding.powermax
org.openhab.binding.pulseaudio
org.openhab.binding.pushbullet
+ org.openhab.binding.radiothermostat
org.openhab.binding.regoheatpump
org.openhab.binding.rfxcom
org.openhab.binding.rme