Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[radiothermostat] Added Absolute Setpoint Mode #8393

Merged
merged 8 commits into from
Sep 9, 2020
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 == "") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be hostName.equals(""), sorry about that.

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