[radiothermostat] Add message display channel and improve ThingActions (#14799)

* Add price message channel

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein 2023-04-15 14:20:20 -05:00 committed by GitHub
parent ec30333314
commit 2c006ccd31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 142 additions and 65 deletions

View File

@ -88,6 +88,7 @@ The thermostat information that is retrieved is available as these channels:
| 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 |
| message | String (Write Only) | Used to display a number in the upper left 'price message' area of the thermostat's screen where the time is normally displayed |
## Full Example
@ -158,15 +159,16 @@ Number Therm_FanStatus "Fan Status [MAP(radiotherm.map):%s_fstus]"
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" }
Number Therm_Day "Thermostat Day [%d]" { channel="radiothermostat:rtherm:mytherm1:day" }
Number Therm_Hour "Thermostat Hour [%d]" { channel="radiothermostat:rtherm:mytherm1:hour" }
Number Therm_Minute "Thermostat Minute [%d]" { 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" }
String Therm_Message "Message: [%s]" { channel="radiothermostat:rtherm:mytherm1:message" }
// Override the thermostat's temperature reading with a value from an external sensor, set to -1 to revert to internal temperature mode
Number:Temperature Therm_Rtemp "Remote Temperature [%d]" <temperature> { channel="radiothermostat:rtherm:mytherm1:remote_temp" }
@ -228,5 +230,30 @@ then
// 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":' + "68" + ', "tmode":1}')
// Also a command can be sent to a specific endpoint on the thermostat by
// specifying it as the second argument to sendRawCommand():
// Reboot the thermostat
// actions.sendRawCommand('{"command": "reboot"}', 'sys/command')
// Control the energy LED (CT80 only) [0 = off, 1 = green, 2 = yellow, 4 = red]
// actions.sendRawCommand('{"energy_led": 1}', 'tstat/led')
// Send a message to the User Message Area (CT80 only)
// actions.sendRawCommand('{"line": 0, "message": "Hello World!"}', 'tstat/uma')
end
rule "Display outside temp in thermostat message area"
when
// An item containing the current outside temperature
Item OutsideTemp changed
then
// Display up to 5 numbers in the thermostat's Price Message Area (PMA)
// A decimal point can be used. CT80 can display a negative '-' number
// Send null or empty string to clear the number and restore the time display
var Number temp = Math.round((OutsideTemp.state as DecimalType).doubleValue).intValue
Therm_Message.sendCommand(temp)
end
```

View File

@ -14,8 +14,6 @@ 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;
@ -41,6 +39,8 @@ public class RadioThermostatBindingConstants {
public static final String PROPERTY_IP = "hostName";
public static final String PROPERTY_ISCT80 = "isCT80";
public static final String JSON_TIME = "{\"day\":%s,\"hour\":%s,\"minute\":%s}";
public static final String JSON_PMA = "{\"line\":1,\"message\":\"%s\"}";
public static final String BLANK = "";
public static final String KEY_ERROR = "error";
@ -52,6 +52,7 @@ public class RadioThermostatBindingConstants {
public static final String TIME_RESOURCE = "tstat/time";
public static final String HEAT_PROGRAM_RESOURCE = "tstat/program/heat";
public static final String COOL_PROGRAM_RESOURCE = "tstat/program/cool";
public static final String PMA_RESOURCE = "tstat/pma";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_RTHERM = new ThingTypeUID(BINDING_ID, "rtherm");
@ -76,12 +77,15 @@ public class RadioThermostatBindingConstants {
public static final String YESTERDAY_HEAT_RUNTIME = "yesterday_heat_runtime";
public static final String YESTERDAY_COOL_RUNTIME = "yesterday_cool_runtime";
public static final String REMOTE_TEMP = "remote_temp";
public static final String MESSAGE = "message";
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, REMOTE_TEMP)
.collect(Collectors.toSet());
public static final Set<String> SUPPORTED_CHANNEL_IDS = Set.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, REMOTE_TEMP, MESSAGE);
public static final Set<String> NO_UPDATE_CHANNEL_IDS = Set.of(REMOTE_TEMP, MESSAGE);
// Units of measurement of the data delivered by the API
public static final Unit<Temperature> API_TEMPERATURE_UNIT = ImperialUnits.FAHRENHEIT;

View File

@ -35,7 +35,7 @@ public class RadioThermostatThingActions implements ThingActions {
private @Nullable RadioThermostatHandler handler;
@RuleAction(label = "send a raw command", description = "Send a raw command to the thermostat.")
@RuleAction(label = "send a raw command", description = "Send a raw command to the thermostat's 'tstat' endpoint.")
public void sendRawCommand(@ActionInput(name = "sendRawCommand") @Nullable String rawCommand) {
if (rawCommand == null) {
logger.warn("sendRawCommand called with null command, ignoring");
@ -49,11 +49,30 @@ public class RadioThermostatThingActions implements ThingActions {
}
}
/** Static alias to support the old DSL rules engine and make the action available there. */
@RuleAction(label = "send a raw command", description = "Send a raw command to a specific endpoint on the thermostat.")
public void sendRawCommand(@ActionInput(name = "sendRawCommand") @Nullable String rawCommand,
@Nullable String resource) {
if (rawCommand == null || resource == null) {
logger.warn("sendRawCommand called with null command, ignoring");
return;
}
RadioThermostatHandler localHandler = handler;
if (localHandler != null) {
localHandler.handleRawCommand(rawCommand, resource);
logger.debug("sendRawCommand called with raw command: {}, resource: {}", rawCommand, resource);
}
}
/** Static aliases to support the old DSL rules engine and make the action available there. */
public static void sendRawCommand(ThingActions actions, @Nullable String rawCommand) {
((RadioThermostatThingActions) actions).sendRawCommand(rawCommand);
}
public static void sendRawCommand(ThingActions actions, @Nullable String rawCommand, @Nullable String resource) {
((RadioThermostatThingActions) actions).sendRawCommand(rawCommand, resource);
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (RadioThermostatHandler) handler;

View File

@ -44,18 +44,18 @@ import org.slf4j.LoggerFactory;
public class RadioThermostatConnector {
private final Logger logger = LoggerFactory.getLogger(RadioThermostatConnector.class);
private static final String URL = "http://%hostName%/%resource%";
private static final String URL = "http://%s/%s";
private final HttpClient httpClient;
private final List<RadioThermostatEventListener> listeners = new CopyOnWriteArrayList<>();
private @Nullable String hostName;
private String hostName = BLANK;
public RadioThermostatConnector(HttpClient httpClient) {
this.httpClient = httpClient;
}
public void setThermostatHostName(@Nullable String hostName) {
public void setThermostatHostName(String hostName) {
this.hostName = hostName;
}
@ -84,19 +84,17 @@ public class RadioThermostatConnector {
* @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(30, TimeUnit.SECONDS).send(new BufferingResponseListener() {
@Override
public void onComplete(@Nullable Result result) {
if (result != null && !result.isFailed()) {
String response = getContentAsString();
dispatchKeyValue(resource, response);
} else {
dispatchKeyValue(KEY_ERROR, "");
}
}
});
httpClient.newRequest(buildRequestURL(resource)).method(GET).timeout(30, TimeUnit.SECONDS)
.send(new BufferingResponseListener() {
@Override
public void onComplete(@Nullable Result result) {
if (result != null && !result.isFailed()) {
dispatchKeyValue(resource, getContentAsString());
} else {
dispatchKeyValue(KEY_ERROR, BLANK);
}
}
});
}
/**
@ -124,30 +122,27 @@ public class RadioThermostatConnector {
String resource) {
// 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(resource);
String output = "";
try {
Request request = httpClient.POST(urlStr);
Request request = httpClient.POST(buildRequestURL(resource));
request.header(HttpHeader.ACCEPT, "text/plain");
request.header(HttpHeader.CONTENT_TYPE, "text/plain");
request.content(new StringContentProvider(postJson), "application/json");
logger.trace("Sending POST request to '{}', data: {}", resource, postJson);
ContentResponse contentResponse = request.send();
int httpStatus = contentResponse.getStatus();
if (httpStatus != OK_200) {
throw new RadioThermostatHttpException("Thermostat HTTP response code was: " + httpStatus);
if (contentResponse.getStatus() != OK_200) {
throw new RadioThermostatHttpException(
"Thermostat HTTP response code was: " + contentResponse.getStatus());
}
output = contentResponse.getContentAsString();
logger.trace("Response: {}", output);
logger.trace("Response: {}", contentResponse.getContentAsString());
return contentResponse.getContentAsString();
} catch (RadioThermostatHttpException | InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Error executing thermostat command: {}, {}", postJson, e.getMessage());
return BLANK;
}
return output;
}
/**
@ -156,14 +151,7 @@ public class RadioThermostatConnector {
* @return a valid URL for the thermostat's JSON interface
*/
private String buildRequestURL(String resource) {
String hostName = this.hostName;
if (hostName == null) {
throw new IllegalStateException("hostname must not be null");
}
String urlStr = URL.replace("%hostName%", hostName);
urlStr = urlStr.replace("%resource%", resource);
return urlStr;
return String.format(URL, hostName, resource);
}
/**

View File

@ -14,7 +14,6 @@ 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;
@ -96,8 +95,8 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
private boolean disableLogs = false;
private boolean clockSync = false;
private String setpointCmdKeyPrefix = "t_";
private String heatProgramJson = "";
private String coolProgramJson = "";
private String heatProgramJson = BLANK;
private String coolProgramJson = BLANK;
public RadioThermostatHandler(Thing thing, RadioThermostatStateDescriptionProvider stateDescriptionProvider,
HttpClient httpClient) {
@ -119,7 +118,7 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
this.disableLogs = config.disableLogs;
this.clockSync = config.clockSync;
if (hostName == null || "".equals(hostName)) {
if (hostName == null || hostName.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error-hostname");
return;
@ -198,17 +197,17 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
Runnable runnable = () -> {
// populate the heat and cool programs on the thermostat from the user configuration,
// the commands will be sent each time the refresh job runs until a success response is seen
if (!"".equals(heatProgramJson)) {
if (!heatProgramJson.isEmpty()) {
final String response = connector.sendCommand(null, null, heatProgramJson, HEAT_PROGRAM_RESOURCE);
if (response.contains("success")) {
heatProgramJson = "";
heatProgramJson = BLANK;
}
}
if (!"".equals(coolProgramJson)) {
if (!coolProgramJson.isEmpty()) {
final String response = connector.sendCommand(null, null, coolProgramJson, COOL_PROGRAM_RESOURCE);
if (response.contains("success")) {
coolProgramJson = "";
coolProgramJson = BLANK;
}
}
@ -307,6 +306,10 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
connector.sendCommand(null, null, rawCommand, DEFAULT_RESOURCE);
}
public void handleRawCommand(@Nullable String rawCommand, String resource) {
connector.sendCommand(null, null, rawCommand, resource);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
@ -384,6 +387,13 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
connector.sendCommand("rem_mode", "0", REMOTE_TEMP_RESOURCE);
}
break;
case MESSAGE:
if (!cmdStr.isEmpty()) {
connector.sendCommand(null, null, String.format(JSON_PMA, cmdStr), PMA_RESOURCE);
} else {
connector.sendCommand("mode", "0", PMA_RESOURCE);
}
break;
default:
logger.warn("Unsupported command: {}", command.toString());
}
@ -460,10 +470,8 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
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 Number) {
state = new DecimalType((Number) value);
} else if (value instanceof String) {
state = new StringType(value.toString());
} else if (value instanceof OnOffType) {
@ -556,9 +564,11 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
*/
private void updateAllChannels() {
// Update all channels from rthermData
for (Channel channel : getThing().getChannels()) {
updateChannel(channel.getUID().getId(), rthermData);
}
getThing().getChannels().forEach(channel -> {
if (!NO_UPDATE_CHANNEL_IDS.contains(channel.getUID().getId())) {
updateChannel(channel.getUID().getId(), rthermData);
}
});
}
/**
@ -569,11 +579,11 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
private List<StateOption> getFanModeOptions() {
List<StateOption> fanModeOptions = new ArrayList<>();
fanModeOptions.add(new StateOption("0", "Auto"));
fanModeOptions.add(new StateOption("0", "@text/options.fan-option-auto"));
if (this.isCT80) {
fanModeOptions.add(new StateOption("1", "Auto/Circulate"));
fanModeOptions.add(new StateOption("1", "@text/options.fan-option-circulate"));
}
fanModeOptions.add(new StateOption("2", "On"));
fanModeOptions.add(new StateOption("2", "@text/options.fan-option-on"));
return fanModeOptions;
}

View File

@ -267,6 +267,8 @@ channel-type.radiothermostat.hold.label = Hold
channel-type.radiothermostat.hold.description = Indicates If the Current Set Point Temperature Is to Be Held Indefinitely
channel-type.radiothermostat.humidity.label = Humidity
channel-type.radiothermostat.humidity.description = The Current Humidity Reading of the Thermostat
channel-type.radiothermostat.message.label = Message
channel-type.radiothermostat.message.description = Use this channel to display a number in the price message area
channel-type.radiothermostat.mode.label = Mode
channel-type.radiothermostat.mode.description = The Current Operating Mode of the HVAC System
channel-type.radiothermostat.mode.state.option.0 = Off
@ -311,3 +313,6 @@ offline.configuration-error-hostname = Thermostat hostname must be specified
offline.configuration-error-heating-program = Thermostat HEATING program schedule is invalid, check configuration!
offline.configuration-error-cooling-program = Thermostat COOLING program schedule is invalid, check configuration!
offline.communication-error-get-data = Error retrieving data from Thermostat
options.fan-option-auto = Auto
options.fan-option-circulate = Auto/Circulate
options.fan-option-on = On

View File

@ -31,10 +31,14 @@
<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"/>
<channel id="message" typeId="message"/>
</channels>
<config-description-ref uri="thing-type:radiothermostat:thermostatconfig"/>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<config-description-ref uri="thing-type:radiothermostat:thermostatconfig"/>
</thing-type>
<channel-type id="temp-temperature">
@ -187,4 +191,10 @@
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="message" advanced="true">
<item-type>String</item-type>
<label>Message</label>
<description>Use this channel to display a number in the price message area</description>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="radiothermostat:rtherm">
<instruction-set targetVersion="1">
<add-channel id="message">
<type>radiothermostat:message</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>