Skip to content

Commit

Permalink
[radiothermostat] Added Absolute Setpoint Mode (#8393)
Browse files Browse the repository at this point in the history
Signed-off-by: Ted Jordan <[email protected]>
  • Loading branch information
silverfunk authored Sep 9, 2020
1 parent 7578fb2 commit 811d329
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 69 deletions.
19 changes: 10 additions & 9 deletions bundles/org.openhab.binding.radiothermostat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ The binding has no configuration options, all configuration is done at Thing lev

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. |
| Parameter | Description |
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| hostName | The host name or IP address of the thermostat. Mandatory. |
| refresh | Overrides the refresh interval of the thermostat data. Optional, the default is 2 minutes. |
| logRefresh | Overrides the refresh interval of the run-time logs & humidity data. Optional, the default is 10 minutes. |
| isCT80 | Flag to enable additional features only available on the CT80 thermostat. Optional, the default is false. |
| disableLogs | Disable retrieval of run-time logs from the thermostat. Optional, the default is false. |
| setpointMode | Controls temporary or absolute setpoint mode. In "temporary" mode the thermostat will temporarily maintain the given setpoint, returning to its program after a time. In "absolute" mode the thermostat will ignore its program maintaining the given setpoint. |

## Channels

Expand Down Expand Up @@ -113,8 +114,8 @@ NULL_over=-
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 ]
radiothermostat:rtherm:mytherm1 "My 1st floor thermostat" [ hostName="192.168.10.1", refresh=2, logRefresh=10, isCT80=false, disableLogs=false, setpointMode="temporary" ]
radiothermostat:rtherm:mytherm2 "My 2nd floor thermostat" [ hostName="mythermhost2", refresh=1, logRefresh=20, isCT80=true, disableLogs=false, setpointMode="absolute" ]
```

radiotherm.items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ public class RadioThermostatConfiguration {
public @Nullable Integer logRefresh;
public boolean isCT80 = false;
public boolean disableLogs = false;
public String setpointMode = "temporary";
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,12 @@
*/
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.binding.BaseDynamicStateDescriptionProvider;
import org.eclipse.smarthome.core.thing.i18n.ChannelTypeI18nLocalizationService;
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;
import org.osgi.service.component.annotations.Reference;

/**
* The {@link RadioThermostatStateDescriptionProvider} class is a dynamic provider of state options while leaving other
Expand All @@ -37,28 +28,15 @@
*/
@Component(service = { DynamicStateDescriptionProvider.class, RadioThermostatStateDescriptionProvider.class })
@NonNullByDefault
public class RadioThermostatStateDescriptionProvider implements DynamicStateDescriptionProvider {
private final Map<ChannelUID, @Nullable List<StateOption>> channelOptionsMap = new ConcurrentHashMap<>();

public void setStateOptions(ChannelUID channelUID, List<StateOption> options) {
channelOptionsMap.put(channelUID, options);
}

@Override
public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
@Nullable Locale locale) {
List<StateOption> 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();
public class RadioThermostatStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}

@Deactivate
public void deactivate() {
channelOptionsMap.clear();
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
private static final int DEFAULT_REFRESH_PERIOD = 2;
private static final int DEFAULT_LOG_REFRESH_PERIOD = 10;

private @Nullable final RadioThermostatStateDescriptionProvider stateDescriptionProvider;

private final RadioThermostatStateDescriptionProvider stateDescriptionProvider;
private final Logger logger = LoggerFactory.getLogger(RadioThermostatHandler.class);

private final Gson gson;
Expand All @@ -87,37 +86,66 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> logRefreshJob;

private @Nullable RadioThermostatConfiguration config;
private int refreshPeriod = DEFAULT_REFRESH_PERIOD;
private int logRefreshPeriod = DEFAULT_LOG_REFRESH_PERIOD;
private boolean isCT80 = false;
private boolean disableLogs = false;
private String setpointCmdKeyPrefix = "t_";

public RadioThermostatHandler(Thing thing,
@Nullable RadioThermostatStateDescriptionProvider stateDescriptionProvider, HttpClient httpClient) {
public RadioThermostatHandler(Thing thing, 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);
RadioThermostatConfiguration config = getConfigAs(RadioThermostatConfiguration.class);

final String hostName = config.hostName;
final Integer refresh = config.refresh;
final Integer logRefresh = config.logRefresh;
this.isCT80 = config.isCT80;
this.disableLogs = config.disableLogs;

if (hostName == null || hostName.equals("")) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Thermostat Host Name must be specified");
return;
}

if (refresh != null) {
this.refreshPeriod = refresh;
}

if (logRefresh != null) {
this.logRefreshPeriod = logRefresh;
}

connector.setThermostatHostName(hostName);
connector.addEventListener(this);

// The setpoint mode is controlled by the name of setpoint attribute sent to the thermostat.
// Temporary mode uses setpoint names prefixed with "t_" while absolute mode uses "a_"
if (config.setpointMode.equals("absolute")) {
this.setpointCmdKeyPrefix = "a_";
}

// populate fan mode options based on thermostat model
List<StateOption> fanModeOptions = getFanModeOptions(config.isCT80);
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), FAN_MODE), fanModeOptions);
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), FAN_MODE), getFanModeOptions());

// if we are not a CT-80, remove the humidity & program mode channel
if (!config.isCT80) {
if (!this.isCT80) {
List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
channels.removeIf(c -> (c.getUID().getId().equals(HUMIDITY)));
channels.removeIf(c -> (c.getUID().getId().equals(PROGRAM_MODE)));
updateThing(editThing().withChannels(channels).build());
}
startAutomaticRefresh();
if (!config.disableLogs || config.isCT80) {
if (!this.disableLogs || this.isCT80) {
startAutomaticLogRefresh();
}

Expand All @@ -132,56 +160,58 @@ public Collection<Class<? extends ThingHandlerService>> getServices() {
/**
* Start the job to periodically update data from the thermostat
*/
@SuppressWarnings("null")
private void startAutomaticRefresh() {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob == null || refreshJob.isCancelled()) {
Runnable runnable = () -> {
// send an async call to the thermostat to get the 'tstat' data
connector.getAsyncThermostatData(DEFAULT_RESOURCE);
};

int delay = (config.refresh != null) ? config.refresh.intValue() : DEFAULT_REFRESH_PERIOD;
refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, delay, TimeUnit.MINUTES);
refreshJob = null;
this.refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refreshPeriod, TimeUnit.MINUTES);
}
}

/**
* Start the job to periodically update humidity and runtime date from the thermostat
*/
@SuppressWarnings("null")
private void startAutomaticLogRefresh() {
ScheduledFuture<?> logRefreshJob = this.logRefreshJob;
if (logRefreshJob == null || logRefreshJob.isCancelled()) {
Runnable runnable = () -> {
// Request humidity data from the thermostat if we are a CT80
if (config.isCT80) {
if (this.isCT80) {
// send an async call to the thermostat to get the humidity data
connector.getAsyncThermostatData(HUMIDITY_RESOURCE);
}

if (!config.disableLogs) {
if (!this.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);
logRefreshJob = null;
this.logRefreshJob = scheduler.scheduleWithFixedDelay(runnable, 1, logRefreshPeriod, TimeUnit.MINUTES);
}
}

@SuppressWarnings("null")
@Override
public void dispose() {
logger.debug("Disposing the RadioThermostat handler.");
connector.removeEventListener(this);

ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob != null) {
refreshJob.cancel(true);
refreshJob = null;
this.refreshJob = null;
}

ScheduledFuture<?> logRefreshJob = this.logRefreshJob;
if (logRefreshJob != null) {
logRefreshJob.cancel(true);
logRefreshJob = null;
this.logRefreshJob = null;
}
}

Expand Down Expand Up @@ -248,10 +278,10 @@ public void handleCommand(ChannelUID channelUID, Command command) {
case SET_POINT:
String cmdKey = null;
if (rthermData.getThermostatData().getMode() == 1) {
cmdKey = "t_heat";
cmdKey = this.setpointCmdKeyPrefix + "heat";
rthermData.getThermostatData().setHeatTarget(cmdInt);
} else if (rthermData.getThermostatData().getMode() == 2) {
cmdKey = "t_cool";
cmdKey = this.setpointCmdKeyPrefix + "cool";
rthermData.getThermostatData().setCoolTarget(cmdInt);
} else {
// don't do anything if we are not in heat or cool mode
Expand Down Expand Up @@ -429,11 +459,11 @@ private void updateAllChannels() {
*
* @return list of state options for thermostat fan modes
*/
private List<StateOption> getFanModeOptions(boolean isCT80) {
private List<StateOption> getFanModeOptions() {
List<StateOption> fanModeOptions = new ArrayList<>();

fanModeOptions.add(new StateOption("0", "Auto"));
if (isCT80) {
if (this.isCT80) {
fanModeOptions.add(new StateOption("1", "Auto/Circulate"));
}
fanModeOptions.add(new StateOption("2", "On"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@
<description>Optional Flag to Disable the Retrieval of Run-time Data from the Thermostat</description>
<default>false</default>
</parameter>
<parameter name="setpointMode" type="text">
<label>Setpoint Mode</label>
<description>Run in absolute or temporary setpoint mode</description>
<default>temporary</default>
<options>
<option value="absolute">Absolute</option>
<option value="temporary">Temporary</option>
</options>
</parameter>

</config-description>
</thing-type>
Expand Down

0 comments on commit 811d329

Please sign in to comment.