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

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
mlobstein 2024-11-16 04:59:00 -06:00 committed by Ciprian Pascu
parent dab00add80
commit f9dee9230e
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.
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_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 |
| 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
@ -152,33 +154,35 @@ radiothermostat:rtherm:mytherm2 "My 2nd floor thermostat" [ hostName="mythermhos
radiotherm.items:
```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
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_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" }
// 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
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 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_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 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: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_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 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", 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_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" }
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_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" }
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" }
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
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_Override 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
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>
<name>openHAB Add-ons :: Bundles :: RadioThermostat Binding</name>
<name>openHAB Add-ons :: Bundles :: Radio Thermostat Binding</name>
</project>

View File

@ -77,12 +77,15 @@ public class RadioThermostatBindingConstants {
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 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<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);
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);

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

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>
<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>
<discovery-methods>

View File

@ -1,7 +1,7 @@
# add-on
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
@ -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.2 = Cool
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.description = Indicates If the Normal Program Setpoint Has Been Manually Overriden
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_cool_runtime" typeId="yesterday_cool_runtime"/>
<channel id="message" typeId="message"/>
<channel id="next_temp" typeId="next_temp"/>
<channel id="next_time" typeId="next_time"/>
</channels>
<properties>
<property name="thingTypeVersion">1</property>
<property name="thingTypeVersion">2</property>
</properties>
<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>
</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>

View File

@ -11,4 +11,15 @@
</instruction-set>
</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>