[radiothermostat] Add next scheduled set point channels (#17743)

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein 2024-11-16 04:59:00 -06:00 committed by GitHub
parent 772026e25e
commit c4bce9a768
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 405 additions and 186 deletions

View File

@ -1,6 +1,6 @@
# RadioThermostat Binding # Radio Thermostat Binding
This binding connects RadioThermostat/3M Filtrete models CT30, CT50/3M50, CT80, etc. with built-in Wi-Fi module to openHAB. This binding connects Radio Thermostat/3M Filtrete models CT30, CT50/3M50, CT80, etc. with built-in Wi-Fi module to openHAB.
Thermostats using a Z-Wave module are not supported but can be used via the openHAB ZWave binding. Thermostats using a Z-Wave module are not supported but can be used via the openHAB ZWave binding.
The binding retrieves and periodically updates all basic system information from the thermostat. The binding retrieves and periodically updates all basic system information from the thermostat.
@ -96,6 +96,8 @@ The thermostat information that is retrieved is available as these channels:
| yesterday_heat_runtime | Number:Time | The total number of minutes of heating run-time yesterday | | 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 | | 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 | | 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 |
| next_temp | Number:Temperature | Displays the next scheduled thermostat set point temperature in the heating or cooling schedule |
| next_time | DateTime | Displays the next scheduled thermostat set point time in the heating or cooling schedule |
## Full Example ## Full Example
@ -152,33 +154,35 @@ radiothermostat:rtherm:mytherm2 "My 2nd floor thermostat" [ hostName="mythermhos
radiotherm.items: radiotherm.items:
```java ```java
Number:Temperature Therm_Temp "Current Temperature [%.1f °F] " <temperature> { channel="radiothermostat:rtherm:mytherm1:temperature" } Number:Temperature Therm_Temp "Current Temperature [%.1f °F]" <temperature> { channel="radiothermostat:rtherm:mytherm1:temperature" }
// Humidity only supported on CT80 // Humidity only supported on CT80
Number Therm_Hum "Current Humidity [%d %%]" <humidity> { channel="radiothermostat:rtherm:mytherm1:humidity" } Number Therm_Hum "Current Humidity [%d %%]" <humidity> { channel="radiothermostat:rtherm:mytherm1:humidity" }
Number Therm_Mode "Thermostat Mode [MAP(radiotherm.map):%s_mode]" { channel="radiothermostat:rtherm:mytherm1:mode" } Number Therm_Mode "Thermostat Mode [MAP(radiotherm.map):%s_mode]" { channel="radiothermostat:rtherm:mytherm1:mode" }
// The Auto/Circulate option will only appear for CT80 // 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" } Number Therm_Fmode "Fan Mode [MAP(radiotherm.map):%s_fan]" { channel="radiothermostat:rtherm:mytherm1:fan_mode" }
// Program Mode only supported on CT80 Rev B // 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 Therm_Pmode "Program Mode [MAP(radiotherm.map):%s_pgm]" { channel="radiothermostat:rtherm:mytherm1:program_mode" }
Number:Temperature Therm_Setpt "Set Point [%d]" <temperature> { channel="radiothermostat:rtherm:mytherm1:set_point" } Number:Temperature Therm_Setpt "Set Point [%d]" <temperature> { channel="radiothermostat:rtherm:mytherm1:set_point" }
Number Therm_Status "Status [MAP(radiotherm.map):%s_stus]" { channel="radiothermostat:rtherm:mytherm1:status" } Number Therm_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_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" } Number Therm_Override "Override [MAP(radiotherm.map):%s_over]" { channel="radiothermostat:rtherm:mytherm1:override" }
Switch Therm_Hold "Hold" { channel="radiothermostat:rtherm:mytherm1:hold" } Switch Therm_Hold "Hold" { channel="radiothermostat:rtherm:mytherm1:hold" }
Number:Temperature Therm_NextTemp "Next Set Temp [%d %unit%]" <temperature> { channel="radiothermostat:rtherm:mytherm1:next_temp" }
DateTime Therm_NextTime "Next Set Time [%1$tl:%1$tM %1$tp]" <time> { channel="radiothermostat:rtherm:mytherm1:next_time" }
Number Therm_Day "Thermostat Day [%d]" { channel="radiothermostat:rtherm:mytherm1:day" } Number Therm_Day "Thermostat Day [%d]" { channel="radiothermostat:rtherm:mytherm1:day" }
Number Therm_Hour "Thermostat Hour [%d]" { channel="radiothermostat:rtherm:mytherm1:hour" } Number Therm_Hour "Thermostat Hour [%d]" { channel="radiothermostat:rtherm:mytherm1:hour" }
Number Therm_Minute "Thermostat Minute [%d]" { channel="radiothermostat:rtherm:mytherm1:minute" } Number Therm_Minute "Thermostat Minute [%d]" { channel="radiothermostat:rtherm:mytherm1:minute" }
String Therm_Dstmp "Thermostat DateStamp [%s]" <time> { channel="radiothermostat:rtherm:mytherm1:dt_stamp" } 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", unit="min" } Number:Time Therm_todayheat "Today's Heating Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:today_heat_runtime", unit="min" }
Number:Time Therm_todaycool "Today's Cooling Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:today_cool_runtime", unit="min" } Number:Time Therm_todaycool "Today's Cooling Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:today_cool_runtime", unit="min" }
Number:Time Therm_yesterdayheat "Yesterday's Heating Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:yesterday_heat_runtime", unit="min" } Number:Time Therm_yesterdayheat "Yesterday's Heating Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:yesterday_heat_runtime", unit="min" }
Number:Time Therm_yesterdaycool "Yesterday's Cooling Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:yesterday_cool_runtime", unit="min" } Number:Time Therm_yesterdaycool "Yesterday's Cooling Runtime [%d %unit%]" { channel="radiothermostat:rtherm:mytherm1:yesterday_cool_runtime", unit="min" }
String Therm_Message "Message: [%s]" { channel="radiothermostat:rtherm:mytherm1:message" } 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 // 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" } Number:Temperature Therm_Rtemp "Remote Temperature [%d]" <temperature> { channel="radiothermostat:rtherm:mytherm1:remote_temp" }
// A virtual switch used to trigger a rule to send a json command to the thermostat // A virtual switch used to trigger a rule to send a json command to the thermostat
Switch Therm_mysetting "Send my preferred setting" Switch Therm_mysetting "Send my preferred setting"
@ -201,6 +205,8 @@ sitemap radiotherm label="My Thermostat" {
Text item=Therm_FanStatus icon="flow" Text item=Therm_FanStatus icon="flow"
Text item=Therm_Override icon="smoke" Text item=Therm_Override icon="smoke"
Switch item=Therm_Hold icon="smoke" Switch item=Therm_Hold icon="smoke"
Text item=Therm_NextTemp icon="temperature"
Text item=Therm_NextTime icon="time"
// Example of overriding the thermostat's temperature reading // Example of overriding the thermostat's temperature reading
Switch item=Therm_Rtemp label="Remote Temp" icon="temperature" mappings=[60="60", 75="75", 80="80", -1="Reset"] Switch item=Therm_Rtemp label="Remote Temp" icon="temperature" mappings=[60="60", 75="75", 80="80", -1="Reset"]

View File

@ -12,6 +12,6 @@
<artifactId>org.openhab.binding.radiothermostat</artifactId> <artifactId>org.openhab.binding.radiothermostat</artifactId>
<name>openHAB Add-ons :: Bundles :: RadioThermostat Binding</name> <name>openHAB Add-ons :: Bundles :: Radio Thermostat Binding</name>
</project> </project>

View File

@ -77,12 +77,15 @@ public class RadioThermostatBindingConstants {
public static final String YESTERDAY_COOL_RUNTIME = "yesterday_cool_runtime"; public static final String YESTERDAY_COOL_RUNTIME = "yesterday_cool_runtime";
public static final String REMOTE_TEMP = "remote_temp"; public static final String REMOTE_TEMP = "remote_temp";
public static final String MESSAGE = "message"; public static final String MESSAGE = "message";
public static final String NEXT_TEMP = "next_temp";
public static final String NEXT_TIME = "next_time";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_RTHERM); public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_RTHERM);
public static final Set<String> SUPPORTED_CHANNEL_IDS = Set.of(TEMPERATURE, HUMIDITY, MODE, FAN_MODE, PROGRAM_MODE, 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, 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); TODAY_COOL_RUNTIME, YESTERDAY_HEAT_RUNTIME, YESTERDAY_COOL_RUNTIME, REMOTE_TEMP, MESSAGE, NEXT_TEMP,
NEXT_TIME);
public static final Set<String> NO_UPDATE_CHANNEL_IDS = Set.of(REMOTE_TEMP, MESSAGE); public static final Set<String> NO_UPDATE_CHANNEL_IDS = Set.of(REMOTE_TEMP, MESSAGE);

View File

@ -39,7 +39,7 @@ import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatHumidityDTO; import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatHumidityDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatRuntimeDTO; import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatRuntimeDTO;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatTstatDTO; import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatTstatDTO;
import org.openhab.binding.radiothermostat.internal.util.RadioThermostatScheduleJson; import org.openhab.binding.radiothermostat.internal.util.RadioThermostatSchedule;
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
@ -83,6 +83,7 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
private final Gson gson; private final Gson gson;
private final RadioThermostatConnector connector; private final RadioThermostatConnector connector;
private final RadioThermostatDTO rthermData = new RadioThermostatDTO(); private final RadioThermostatDTO rthermData = new RadioThermostatDTO();
private @Nullable RadioThermostatSchedule thermostatSchedule;
private @Nullable ScheduledFuture<?> refreshJob; private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> logRefreshJob; private @Nullable ScheduledFuture<?> logRefreshJob;
@ -151,10 +152,10 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
updateThing(editThing().withChannels(channels).build()); updateThing(editThing().withChannels(channels).build());
} }
final RadioThermostatScheduleJson thermostatSchedule = new RadioThermostatScheduleJson(config); final RadioThermostatSchedule localSchedule = thermostatSchedule = new RadioThermostatSchedule(config);
try { try {
heatProgramJson = thermostatSchedule.getHeatProgramJson(); heatProgramJson = localSchedule.getHeatProgramJson();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error-heating-program"); "@text/offline.configuration-error-heating-program");
@ -162,7 +163,7 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
} }
try { try {
coolProgramJson = thermostatSchedule.getCoolProgramJson(); coolProgramJson = localSchedule.getCoolProgramJson();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error-cooling-program"); "@text/offline.configuration-error-cooling-program");
@ -359,6 +360,8 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
updateChannel(SET_POINT, rthermData); updateChannel(SET_POINT, rthermData);
rthermData.getThermostatData().setHold(0); rthermData.getThermostatData().setHold(0);
updateChannel(HOLD, rthermData); updateChannel(HOLD, rthermData);
updateChannel(NEXT_TEMP, rthermData);
updateChannel(NEXT_TIME, rthermData);
rthermData.getThermostatData().setProgramMode(-1); rthermData.getThermostatData().setProgramMode(-1);
updateChannel(PROGRAM_MODE, rthermData); updateChannel(PROGRAM_MODE, rthermData);
@ -383,6 +386,8 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
rthermData.getThermostatData().setHold(0); rthermData.getThermostatData().setHold(0);
connector.sendCommand("hold", "0", DEFAULT_RESOURCE); connector.sendCommand("hold", "0", DEFAULT_RESOURCE);
} }
updateChannel(NEXT_TEMP, rthermData);
updateChannel(NEXT_TIME, rthermData);
break; break;
case SET_POINT: case SET_POINT:
String cmdKey; String cmdKey;
@ -404,7 +409,10 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
if (cmdInt != -1) { if (cmdInt != -1) {
QuantityType<?> remoteTemp = ((QuantityType<Temperature>) command) QuantityType<?> remoteTemp = ((QuantityType<Temperature>) command)
.toUnit(ImperialUnits.FAHRENHEIT); .toUnit(ImperialUnits.FAHRENHEIT);
connector.sendCommand("rem_temp", String.valueOf(remoteTemp.intValue()), REMOTE_TEMP_RESOURCE); if (remoteTemp != null) {
connector.sendCommand("rem_temp", String.valueOf(remoteTemp.intValue()),
REMOTE_TEMP_RESOURCE);
}
} else { } else {
connector.sendCommand("rem_mode", "0", REMOTE_TEMP_RESOURCE); connector.sendCommand("rem_mode", "0", REMOTE_TEMP_RESOURCE);
} }
@ -477,7 +485,7 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
if (isLinked(channelId)) { if (isLinked(channelId)) {
Object value; Object value;
try { try {
value = getValue(channelId, rthermData); value = getValue(channelId, rthermData, thermostatSchedule);
} catch (Exception e) { } catch (Exception e) {
logger.debug("Error setting {} value", channelId.toUpperCase()); logger.debug("Error setting {} value", channelId.toUpperCase());
return; return;
@ -519,7 +527,8 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
* @param data the RadioThermostat dto * @param data the RadioThermostat dto
* @return the value to be set in the state * @return the value to be set in the state
*/ */
public static @Nullable Object getValue(String channelId, RadioThermostatDTO data) { public static @Nullable Object getValue(String channelId, RadioThermostatDTO data,
@Nullable RadioThermostatSchedule thermostatSchedule) {
switch (channelId) { switch (channelId) {
case TEMPERATURE: case TEMPERATURE:
if (data.getThermostatData().getTemperature() != null) { if (data.getThermostatData().getTemperature() != null) {
@ -576,6 +585,22 @@ public class RadioThermostatHandler extends BaseThingHandler implements RadioThe
case YESTERDAY_COOL_RUNTIME: case YESTERDAY_COOL_RUNTIME:
return new QuantityType<>(data.getRuntime().getYesterday().getCoolTime().getRuntime(), return new QuantityType<>(data.getRuntime().getYesterday().getCoolTime().getRuntime(),
API_MINUTES_UNIT); API_MINUTES_UNIT);
case NEXT_TEMP:
if (thermostatSchedule != null) {
final Integer nextTemp = thermostatSchedule.getNextTemp(data.getThermostatData());
if (nextTemp != null) {
return new QuantityType<>(nextTemp, API_TEMPERATURE_UNIT);
}
}
return null;
case NEXT_TIME:
if (thermostatSchedule != null) {
final ZonedDateTime nextTime = thermostatSchedule.getNextTime(data.getThermostatData());
if (nextTime != null) {
return nextTime;
}
}
return null;
} }
return null; return null;
} }

View File

@ -0,0 +1,296 @@
/**
* Copyright (c) 2010-2024 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.util;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.radiothermostat.internal.RadioThermostatConfiguration;
import org.openhab.binding.radiothermostat.internal.dto.RadioThermostatTstatDTO;
/**
* The {@link RadioThermostatSchedule} is the class used to convert the heating and cooling schedules from user
* configuration into the json that is sent to the thermostat
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RadioThermostatSchedule {
private final ArrayList<DaySchedule> heatSchedule = new ArrayList<DaySchedule>();
private final ArrayList<DaySchedule> coolSchedule = new ArrayList<DaySchedule>();
public RadioThermostatSchedule(RadioThermostatConfiguration config) {
heatSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.monMorningHeatTime, config.monMorningHeatTemp),
new SetPeriod(config.monDayHeatTime, config.monDayHeatTemp),
new SetPeriod(config.monEveningHeatTime, config.monEveningHeatTemp),
new SetPeriod(config.monNightHeatTime, config.monNightHeatTemp)))));
heatSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.tueMorningHeatTime, config.tueMorningHeatTemp),
new SetPeriod(config.tueDayHeatTime, config.tueDayHeatTemp),
new SetPeriod(config.tueEveningHeatTime, config.tueEveningHeatTemp),
new SetPeriod(config.tueNightHeatTime, config.tueNightHeatTemp)))));
heatSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.wedMorningHeatTime, config.wedMorningHeatTemp),
new SetPeriod(config.wedDayHeatTime, config.wedDayHeatTemp),
new SetPeriod(config.wedEveningHeatTime, config.wedEveningHeatTemp),
new SetPeriod(config.wedNightHeatTime, config.wedNightHeatTemp)))));
heatSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.thuMorningHeatTime, config.thuMorningHeatTemp),
new SetPeriod(config.thuDayHeatTime, config.thuDayHeatTemp),
new SetPeriod(config.thuEveningHeatTime, config.thuEveningHeatTemp),
new SetPeriod(config.thuNightHeatTime, config.thuNightHeatTemp)))));
heatSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.friMorningHeatTime, config.friMorningHeatTemp),
new SetPeriod(config.friDayHeatTime, config.friDayHeatTemp),
new SetPeriod(config.friEveningHeatTime, config.friEveningHeatTemp),
new SetPeriod(config.friNightHeatTime, config.friNightHeatTemp)))));
heatSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.satMorningHeatTime, config.satMorningHeatTemp),
new SetPeriod(config.satDayHeatTime, config.satDayHeatTemp),
new SetPeriod(config.satEveningHeatTime, config.satEveningHeatTemp),
new SetPeriod(config.satNightHeatTime, config.satNightHeatTemp)))));
heatSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.sunMorningHeatTime, config.sunMorningHeatTemp),
new SetPeriod(config.sunDayHeatTime, config.sunDayHeatTemp),
new SetPeriod(config.sunEveningHeatTime, config.sunEveningHeatTemp),
new SetPeriod(config.sunNightHeatTime, config.sunNightHeatTemp)))));
coolSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.monMorningCoolTime, config.monMorningCoolTemp),
new SetPeriod(config.monDayCoolTime, config.monDayCoolTemp),
new SetPeriod(config.monEveningCoolTime, config.monEveningCoolTemp),
new SetPeriod(config.monNightCoolTime, config.monNightCoolTemp)))));
coolSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.tueMorningCoolTime, config.tueMorningCoolTemp),
new SetPeriod(config.tueDayCoolTime, config.tueDayCoolTemp),
new SetPeriod(config.tueEveningCoolTime, config.tueEveningCoolTemp),
new SetPeriod(config.tueNightCoolTime, config.tueNightCoolTemp)))));
coolSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.wedMorningCoolTime, config.wedMorningCoolTemp),
new SetPeriod(config.wedDayCoolTime, config.wedDayCoolTemp),
new SetPeriod(config.wedEveningCoolTime, config.wedEveningCoolTemp),
new SetPeriod(config.wedNightCoolTime, config.wedNightCoolTemp)))));
coolSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.thuMorningCoolTime, config.thuMorningCoolTemp),
new SetPeriod(config.thuDayCoolTime, config.thuDayCoolTemp),
new SetPeriod(config.thuEveningCoolTime, config.thuEveningCoolTemp),
new SetPeriod(config.thuNightCoolTime, config.thuNightCoolTemp)))));
coolSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.friMorningCoolTime, config.friMorningCoolTemp),
new SetPeriod(config.friDayCoolTime, config.friDayCoolTemp),
new SetPeriod(config.friEveningCoolTime, config.friEveningCoolTemp),
new SetPeriod(config.friNightCoolTime, config.friNightCoolTemp)))));
coolSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.satMorningCoolTime, config.satMorningCoolTemp),
new SetPeriod(config.satDayCoolTime, config.satDayCoolTemp),
new SetPeriod(config.satEveningCoolTime, config.satEveningCoolTemp),
new SetPeriod(config.satNightCoolTime, config.satNightCoolTemp)))));
coolSchedule.add(new DaySchedule(
new ArrayList<SetPeriod>(List.of(new SetPeriod(config.sunMorningCoolTime, config.sunMorningCoolTemp),
new SetPeriod(config.sunDayCoolTime, config.sunDayCoolTemp),
new SetPeriod(config.sunEveningCoolTime, config.sunEveningCoolTemp),
new SetPeriod(config.sunNightCoolTime, config.sunNightCoolTemp)))));
}
public String getHeatProgramJson() throws IllegalStateException {
return getProgramJson(heatSchedule);
}
public String getCoolProgramJson() throws IllegalStateException {
return getProgramJson(coolSchedule);
}
private String getProgramJson(ArrayList<DaySchedule> schedule) throws IllegalStateException {
// all were null, bypass
if (schedule.stream().allMatch(day -> day.isAnyNull())) {
return "";
}
// some were null, the schedule is invalid
if (schedule.stream().anyMatch(day -> day.isAnyNull())) {
throw new IllegalStateException();
}
final StringBuilder json = new StringBuilder("{");
IntStream.range(0, 7).forEach(i -> {
json.append("\"" + i + "\":" + getDaySchedule(schedule.get(i)));
if (i < 6) {
json.append(",");
}
});
json.append("}");
return json.toString();
}
private @Nullable String getDaySchedule(DaySchedule day) {
// if any of the time or temp fields are null, this day schedule is not valid
if (day.isAnyNull()) {
return null;
}
final ArrayList<SetPeriod> setPeriods = day.getSchedule();
final int morningMin;
final int dayMin;
final int eveningMin;
final int nightMin;
try {
morningMin = setPeriods.get(0).getMinutes();
dayMin = setPeriods.get(1).getMinutes();
eveningMin = setPeriods.get(2).getMinutes();
nightMin = setPeriods.get(3).getMinutes();
} catch (NumberFormatException nfe) {
// if any of the times could not be parsed into minutes, the schedule is invalid
return null;
}
// the minute value for each period must be greater than the previous period otherwise the schedule is invalid
if (morningMin >= dayMin || dayMin >= eveningMin || eveningMin >= nightMin) {
return null;
}
return "[" + morningMin + "," + setPeriods.get(0).getTemp() + "," + dayMin + "," + setPeriods.get(1).getTemp()
+ "," + eveningMin + "," + setPeriods.get(2).getTemp() + "," + nightMin + ","
+ setPeriods.get(3).getTemp() + "]";
}
public class DaySchedule {
private final ArrayList<SetPeriod> schedule;
public DaySchedule(ArrayList<SetPeriod> schedule) {
this.schedule = schedule;
}
public ArrayList<SetPeriod> getSchedule() {
return schedule;
}
public boolean isAnyNull() {
return schedule.stream().anyMatch(itm -> itm.getTime() == null || itm.getTemp() == null);
}
}
public class SetPeriod {
private final @Nullable String time;
private final @Nullable Integer temp;
public SetPeriod(@Nullable String time, @Nullable Integer temp) {
this.time = time;
this.temp = temp;
}
public @Nullable ZonedDateTime getTime() {
final String timeLocal = time;
if (timeLocal != null) {
final String[] hourMin = timeLocal.split(":");
// get a zdt with the hour and minute of the next set point
final ZonedDateTime nextSetTime = ZonedDateTime.now().withHour(Integer.parseInt(hourMin[0]))
.withMinute(Integer.parseInt(hourMin[1])).withSecond(0).withNano(0);
// if the next set point occurs tomorrow, add one day to the zdt
if (nextSetTime.isBefore(ZonedDateTime.now())) {
return nextSetTime.plusDays(1);
}
return nextSetTime;
}
return null;
}
public @Nullable Integer getTemp() {
return temp;
}
public int getMinutes() {
final String timeLocal = time;
final String[] hourMin = timeLocal != null ? timeLocal.split(":") : new String[] { "" };
return Integer.parseInt(hourMin[0]) * 60 + Integer.parseInt(hourMin[1]);
}
}
public @Nullable Integer getNextTemp(RadioThermostatTstatDTO thermostatData) {
final SetPeriod nextPeriod = getNextSetpoint(thermostatData);
return nextPeriod != null ? nextPeriod.getTemp() : null;
}
public @Nullable ZonedDateTime getNextTime(RadioThermostatTstatDTO thermostatData) {
final SetPeriod nextPeriod = getNextSetpoint(thermostatData);
return nextPeriod != null ? nextPeriod.getTime() : null;
}
private @Nullable SetPeriod getNextSetpoint(RadioThermostatTstatDTO thermostatData) {
if (thermostatData.getHold().equals(1)) {
return null;
}
final ArrayList<DaySchedule> schedule;
if (thermostatData.getMode().equals(1)) {
schedule = heatSchedule;
} else if (thermostatData.getMode().equals(2)) {
schedule = coolSchedule;
} else {
return null;
}
final DaySchedule daySched = schedule.get(thermostatData.getTime().getDayOfWeek().intValue());
int nextPeriod = 0;
try {
for (int i = 0; i <= 3; i++) {
if (thermostatData.getTime().getRuntime().intValue() >= daySched.getSchedule().get(i).getMinutes()) {
if (i == 3) {
nextPeriod = -1;
break;
} else {
nextPeriod = i + 1;
}
}
}
} catch (NumberFormatException e) {
return null;
}
if (nextPeriod == -1) {
int nextDay = thermostatData.getTime().getDayOfWeek().intValue() + 1;
if (nextDay == 7) {
nextDay = 0;
}
return schedule.get(nextDay).getSchedule().get(0);
}
return daySched.getSchedule().get(nextPeriod);
}
}

View File

@ -1,152 +0,0 @@
/**
* Copyright (c) 2010-2024 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.util;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.radiothermostat.internal.RadioThermostatConfiguration;
/**
* The {@link RadioThermostatScheduleJson} is the class used to convert the heating and cooling schedules from user
* configuration into the json that is sent to the thermostat
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class RadioThermostatScheduleJson {
private final @Nullable String monHeat;
private final @Nullable String tueHeat;
private final @Nullable String wedHeat;
private final @Nullable String thuHeat;
private final @Nullable String friHeat;
private final @Nullable String satHeat;
private final @Nullable String sunHeat;
private final @Nullable String monCool;
private final @Nullable String tueCool;
private final @Nullable String wedCool;
private final @Nullable String thuCool;
private final @Nullable String friCool;
private final @Nullable String satCool;
private final @Nullable String sunCool;
public RadioThermostatScheduleJson(RadioThermostatConfiguration config) {
monHeat = getDaySchedule(config.monMorningHeatTime, config.monDayHeatTime, config.monEveningHeatTime,
config.monNightHeatTime, config.monMorningHeatTemp, config.monDayHeatTemp, config.monEveningHeatTemp,
config.monNightHeatTemp);
tueHeat = getDaySchedule(config.tueMorningHeatTime, config.tueDayHeatTime, config.tueEveningHeatTime,
config.tueNightHeatTime, config.tueMorningHeatTemp, config.tueDayHeatTemp, config.tueEveningHeatTemp,
config.tueNightHeatTemp);
wedHeat = getDaySchedule(config.wedMorningHeatTime, config.wedDayHeatTime, config.wedEveningHeatTime,
config.wedNightHeatTime, config.wedMorningHeatTemp, config.wedDayHeatTemp, config.wedEveningHeatTemp,
config.wedNightHeatTemp);
thuHeat = getDaySchedule(config.thuMorningHeatTime, config.thuDayHeatTime, config.thuEveningHeatTime,
config.thuNightHeatTime, config.thuMorningHeatTemp, config.thuDayHeatTemp, config.thuEveningHeatTemp,
config.thuNightHeatTemp);
friHeat = getDaySchedule(config.friMorningHeatTime, config.friDayHeatTime, config.friEveningHeatTime,
config.friNightHeatTime, config.friMorningHeatTemp, config.friDayHeatTemp, config.friEveningHeatTemp,
config.friNightHeatTemp);
satHeat = getDaySchedule(config.satMorningHeatTime, config.satDayHeatTime, config.satEveningHeatTime,
config.satNightHeatTime, config.satMorningHeatTemp, config.satDayHeatTemp, config.satEveningHeatTemp,
config.satNightHeatTemp);
sunHeat = getDaySchedule(config.sunMorningHeatTime, config.sunDayHeatTime, config.sunEveningHeatTime,
config.sunNightHeatTime, config.sunMorningHeatTemp, config.sunDayHeatTemp, config.sunEveningHeatTemp,
config.sunNightHeatTemp);
monCool = getDaySchedule(config.monMorningCoolTime, config.monDayCoolTime, config.monEveningCoolTime,
config.monNightCoolTime, config.monMorningCoolTemp, config.monDayCoolTemp, config.monEveningCoolTemp,
config.monNightCoolTemp);
tueCool = getDaySchedule(config.tueMorningCoolTime, config.tueDayCoolTime, config.tueEveningCoolTime,
config.tueNightCoolTime, config.tueMorningCoolTemp, config.tueDayCoolTemp, config.tueEveningCoolTemp,
config.tueNightCoolTemp);
wedCool = getDaySchedule(config.wedMorningCoolTime, config.wedDayCoolTime, config.wedEveningCoolTime,
config.wedNightCoolTime, config.wedMorningCoolTemp, config.wedDayCoolTemp, config.wedEveningCoolTemp,
config.wedNightCoolTemp);
thuCool = getDaySchedule(config.thuMorningCoolTime, config.thuDayCoolTime, config.thuEveningCoolTime,
config.thuNightCoolTime, config.thuMorningCoolTemp, config.thuDayCoolTemp, config.thuEveningCoolTemp,
config.thuNightCoolTemp);
friCool = getDaySchedule(config.friMorningCoolTime, config.friDayCoolTime, config.friEveningCoolTime,
config.friNightCoolTime, config.friMorningCoolTemp, config.friDayCoolTemp, config.friEveningCoolTemp,
config.friNightCoolTemp);
satCool = getDaySchedule(config.satMorningCoolTime, config.satDayCoolTime, config.satEveningCoolTime,
config.satNightCoolTime, config.satMorningCoolTemp, config.satDayCoolTemp, config.satEveningCoolTemp,
config.satNightCoolTemp);
sunCool = getDaySchedule(config.sunMorningCoolTime, config.sunDayCoolTime, config.sunEveningCoolTime,
config.sunNightCoolTime, config.sunMorningCoolTemp, config.sunDayCoolTemp, config.sunEveningCoolTemp,
config.sunNightCoolTemp);
}
public String getHeatProgramJson() throws IllegalStateException {
return getProgramJson(monHeat, tueHeat, wedHeat, thuHeat, friHeat, satHeat, sunHeat);
}
public String getCoolProgramJson() throws IllegalStateException {
return getProgramJson(monCool, tueCool, wedCool, thuCool, friCool, satCool, sunCool);
}
private String getProgramJson(@Nullable String mon, @Nullable String tue, @Nullable String wed,
@Nullable String thu, @Nullable String fri, @Nullable String sat, @Nullable String sun)
throws IllegalStateException {
// all were null, bypass
if (mon == null && tue == null && wed == null && thu == null && fri == null && sat == null && sun == null) {
return "";
}
// some were null, the schedule is invalid
if (mon == null || tue == null || wed == null || thu == null || fri == null || sat == null || sun == null) {
throw new IllegalStateException();
}
return "{\"0\":" + mon + ",\"1\":" + tue + ",\"2\":" + wed + ",\"3\":" + thu + ",\"4\":" + fri + ",\"5\":" + sat
+ ",\"6\":" + sun + "}";
}
private @Nullable String getDaySchedule(@Nullable String morningTime, @Nullable String dayTime,
@Nullable String eveningTime, @Nullable String nightTime, @Nullable Integer morningTemp,
@Nullable Integer dayTemp, @Nullable Integer eveningTemp, @Nullable Integer nightTemp) {
// if any null, this day schedule is not valid
if (morningTime == null || dayTime == null || eveningTime == null || nightTime == null || morningTemp == null
|| dayTemp == null || eveningTemp == null || nightTemp == null) {
return null;
}
final int morningMin;
final int dayMin;
final int eveningMin;
final int nightMin;
try {
morningMin = parseMinutes(morningTime);
dayMin = parseMinutes(dayTime);
eveningMin = parseMinutes(eveningTime);
nightMin = parseMinutes(nightTime);
} catch (NumberFormatException nfe) {
// if any of the times could not be parsed into minutes, the schedule is invalid
return null;
}
// the minute value for each period must be greater than the previous period otherwise the schedule is invalid
if (morningMin >= dayMin || dayMin >= eveningMin || eveningMin >= nightMin) {
return null;
}
return "[" + morningMin + "," + morningTemp + "," + dayMin + "," + dayTemp + "," + eveningMin + ","
+ eveningTemp + "," + nightMin + "," + nightTemp + "]";
}
private int parseMinutes(String timeStr) {
final String[] hourMin = timeStr.split(":");
return Integer.parseInt(hourMin[0]) * 60 + Integer.parseInt(hourMin[1]);
}
}

View File

@ -5,7 +5,7 @@
<type>binding</type> <type>binding</type>
<name>Radio Thermostat Binding</name> <name>Radio Thermostat Binding</name>
<description>Controls the RadioThermostat model CT30, CT50 or CT80 via the built-in WIFI module</description> <description>Controls the Radio Thermostat model CT30, CT50 or CT80 via the built-in WIFI module</description>
<connection>local</connection> <connection>local</connection>
<discovery-methods> <discovery-methods>

View File

@ -1,7 +1,7 @@
# add-on # add-on
addon.radiothermostat.name = Radio Thermostat Binding addon.radiothermostat.name = Radio Thermostat Binding
addon.radiothermostat.description = Controls the RadioThermostat model CT30, CT50 or CT80 via the built-in WIFI module addon.radiothermostat.description = Controls the Radio Thermostat model CT30, CT50 or CT80 via the built-in WIFI module
# thing types # thing types
@ -275,6 +275,10 @@ channel-type.radiothermostat.mode.state.option.0 = Off
channel-type.radiothermostat.mode.state.option.1 = Heat channel-type.radiothermostat.mode.state.option.1 = Heat
channel-type.radiothermostat.mode.state.option.2 = Cool channel-type.radiothermostat.mode.state.option.2 = Cool
channel-type.radiothermostat.mode.state.option.3 = Auto channel-type.radiothermostat.mode.state.option.3 = Auto
channel-type.radiothermostat.next_temp.label = Next Set Temp
channel-type.radiothermostat.next_temp.description = Displays the next scheduled thermostat set point temperature
channel-type.radiothermostat.next_time.label = Next Set Time
channel-type.radiothermostat.next_time.description = Displays the next scheduled thermostat set point time
channel-type.radiothermostat.override.label = Override channel-type.radiothermostat.override.label = Override
channel-type.radiothermostat.override.description = Indicates If the Normal Program Setpoint Has Been Manually Overriden channel-type.radiothermostat.override.description = Indicates If the Normal Program Setpoint Has Been Manually Overriden
channel-type.radiothermostat.program_mode.label = Program Mode channel-type.radiothermostat.program_mode.label = Program Mode

View File

@ -32,10 +32,12 @@
<channel id="yesterday_heat_runtime" typeId="yesterday_heat_runtime"/> <channel id="yesterday_heat_runtime" typeId="yesterday_heat_runtime"/>
<channel id="yesterday_cool_runtime" typeId="yesterday_cool_runtime"/> <channel id="yesterday_cool_runtime" typeId="yesterday_cool_runtime"/>
<channel id="message" typeId="message"/> <channel id="message" typeId="message"/>
<channel id="next_temp" typeId="next_temp"/>
<channel id="next_time" typeId="next_time"/>
</channels> </channels>
<properties> <properties>
<property name="thingTypeVersion">1</property> <property name="thingTypeVersion">2</property>
</properties> </properties>
<config-description-ref uri="thing-type:radiothermostat:thermostatconfig"/> <config-description-ref uri="thing-type:radiothermostat:thermostatconfig"/>
@ -197,4 +199,28 @@
<description>Use this channel to display a number in the price message area</description> <description>Use this channel to display a number in the price message area</description>
</channel-type> </channel-type>
<channel-type id="next_temp" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Next Set Temp</label>
<description>Displays the next scheduled thermostat set point temperature</description>
<category>Temperature</category>
<tags>
<tag>Setpoint</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="next_time" advanced="true">
<item-type>DateTime</item-type>
<label>Next Set Time</label>
<description>Displays the next scheduled thermostat set point time</description>
<category>Time</category>
<tags>
<tag>Status</tag>
<tag>Timestamp</tag>
</tags>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions> </thing:thing-descriptions>

View File

@ -11,4 +11,15 @@
</instruction-set> </instruction-set>
</thing-type> </thing-type>
<thing-type uid="radiothermostat:rtherm">
<instruction-set targetVersion="2">
<add-channel id="next_temp">
<type>radiothermostat:next_temp</type>
</add-channel>
<add-channel id="next_time">
<type>radiothermostat:next_time</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions> </update:update-descriptions>