[daikin] Add demand control support for ac_unit (#17087)

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
jimtng 2024-07-26 19:41:55 +10:00 committed by GitHub
parent b0f9d82c1e
commit 7e73ed83cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 984 additions and 60 deletions

View File

@ -45,50 +45,53 @@ A BRP072C42 adapter requires a registered UUID to authenticate. Upon discovery,
The temperature channels have a precision of one half degree Celsius.
For the BRP072A42 and BRP072C42:
| Channel Name | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| power | Turns the power on/off for the air conditioning unit. |
| settemp | The temperature set for the air conditioning unit. |
| indoortemp | The indoor temperature as measured by the unit. |
| outdoortemp | The outdoor temperature as measured by the external part of the air conditioning system. May not be available when unit is off. |
| humidity | The indoor humidity as measured by the unit. This is not available on all units. |
| mode | The mode set for the unit (AUTO, DEHUMIDIFIER, COLD, HEAT, FAN) |
| homekitmode | A mode that is compatible with homekit/alexa/google home (off, auto, heat, cool). Not tested for BRP069B41 |
| fanspeed | The fan speed set for the unit (AUTO, SILENCE, LEVEL_1, LEVEL_2, LEVEL_3, LEVEL_4, LEVEL_5) |
| fandir | The fan blade direction (STOPPED, VERTICAL, HORIZONTAL, VERTICAL_AND_HORIZONTAL) |
| cmpfrequency | The compressor frequency |
| specialmode | The special mode set for the unit (NORMAL, ECO, POWERFUL). This is not available on all units. |
| streamer | Turns the streamer feature on/off for the air conditioning unit. This is not available on all units. |
| energyheatingtoday | The energy consumption when heating for today |
| energyheatingthisweek | The energy consumption when heating for this week |
| energyheatinglastweek | The energy consumption when heating for last week |
| energyheatingcurrentyear-1 | The energy consumption when heating for current year January |
| energyheatingcurrentyear-2 | The energy consumption when heating for current year February |
| energyheatingcurrentyear-3 | The energy consumption when heating for current year March |
| energyheatingcurrentyear-4 | The energy consumption when heating for current year April |
| energyheatingcurrentyear-5 | The energy consumption when heating for current year May |
| energyheatingcurrentyear-6 | The energy consumption when heating for current year June |
| energyheatingcurrentyear-7 | The energy consumption when heating for current year July |
| energyheatingcurrentyear-8 | The energy consumption when heating for current year August |
| energyheatingcurrentyear-9 | The energy consumption when heating for current year September |
| energyheatingcurrentyear-10 | The energy consumption when heating for current year October |
| energyheatingcurrentyear-11 | The energy consumption when heating for current year November |
| energyheatingcurrentyear-12 | The energy consumption when heating for current year December |
| energycoolingtoday | The energy consumption when cooling for today |
| energycoolingthisweek | The energy consumption when cooling for this week |
| energycoolinglastweek | The energy consumption when cooling for last week |
| energycoolingcurrentyear-1 | The energy consumption when cooling for current year January |
| energycoolingcurrentyear-2 | The energy consumption when cooling for current year February |
| energycoolingcurrentyear-3 | The energy consumption when cooling for current year March |
| energycoolingcurrentyear-4 | The energy consumption when cooling for current year April |
| energycoolingcurrentyear-5 | The energy consumption when cooling for current year May |
| energycoolingcurrentyear-6 | The energy consumption when cooling for current year June |
| energycoolingcurrentyear-7 | The energy consumption when cooling for current year July |
| energycoolingcurrentyear-8 | The energy consumption when cooling for current year August |
| energycoolingcurrentyear-9 | The energy consumption when cooling for current year September |
| energycoolingcurrentyear-10 | The energy consumption when cooling for current year October |
| energycoolingcurrentyear-11 | The energy consumption when cooling for current year November |
| energycoolingcurrentyear-12 | The energy consumption when cooling for current year December |
| Channel Name | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| power | Turns the power on/off for the air conditioning unit. |
| settemp | The temperature set for the air conditioning unit. |
| indoortemp | The indoor temperature as measured by the unit. |
| outdoortemp | The outdoor temperature as measured by the external part of the air conditioning system. May not be available when unit is off. |
| humidity | The indoor humidity as measured by the unit. This is not available on all units. |
| mode | The mode set for the unit (AUTO, DEHUMIDIFIER, COLD, HEAT, FAN) |
| homekitmode | A mode that is compatible with homekit/alexa/google home (off, auto, heat, cool). Not tested for BRP069B41 |
| fanspeed | The fan speed set for the unit (AUTO, SILENCE, LEVEL_1, LEVEL_2, LEVEL_3, LEVEL_4, LEVEL_5) |
| fandir | The fan blade direction (STOPPED, VERTICAL, HORIZONTAL, VERTICAL_AND_HORIZONTAL) |
| cmpfrequency | The compressor frequency |
| specialmode | The special mode set for the unit (NORMAL, ECO, POWERFUL). This is not available on all units. |
| streamer | Turns the streamer feature on/off for the air conditioning unit. This is not available on all units. |
| energyheatingtoday | The energy consumption when heating for today |
| energyheatingthisweek | The energy consumption when heating for this week |
| energyheatinglastweek | The energy consumption when heating for last week |
| energyheatingcurrentyear-1 | The energy consumption when heating for current year January |
| energyheatingcurrentyear-2 | The energy consumption when heating for current year February |
| energyheatingcurrentyear-3 | The energy consumption when heating for current year March |
| energyheatingcurrentyear-4 | The energy consumption when heating for current year April |
| energyheatingcurrentyear-5 | The energy consumption when heating for current year May |
| energyheatingcurrentyear-6 | The energy consumption when heating for current year June |
| energyheatingcurrentyear-7 | The energy consumption when heating for current year July |
| energyheatingcurrentyear-8 | The energy consumption when heating for current year August |
| energyheatingcurrentyear-9 | The energy consumption when heating for current year September |
| energyheatingcurrentyear-10 | The energy consumption when heating for current year October |
| energyheatingcurrentyear-11 | The energy consumption when heating for current year November |
| energyheatingcurrentyear-12 | The energy consumption when heating for current year December |
| energycoolingtoday | The energy consumption when cooling for today |
| energycoolingthisweek | The energy consumption when cooling for this week |
| energycoolinglastweek | The energy consumption when cooling for last week |
| energycoolingcurrentyear-1 | The energy consumption when cooling for current year January |
| energycoolingcurrentyear-2 | The energy consumption when cooling for current year February |
| energycoolingcurrentyear-3 | The energy consumption when cooling for current year March |
| energycoolingcurrentyear-4 | The energy consumption when cooling for current year April |
| energycoolingcurrentyear-5 | The energy consumption when cooling for current year May |
| energycoolingcurrentyear-6 | The energy consumption when cooling for current year June |
| energycoolingcurrentyear-7 | The energy consumption when cooling for current year July |
| energycoolingcurrentyear-8 | The energy consumption when cooling for current year August |
| energycoolingcurrentyear-9 | The energy consumption when cooling for current year September |
| energycoolingcurrentyear-10 | The energy consumption when cooling for current year October |
| energycoolingcurrentyear-11 | The energy consumption when cooling for current year November |
| energycoolingcurrentyear-12 | The energy consumption when cooling for current year December |
| demandcontrolmode | The demand control mode (`OFF`, `AUTO`, `MANUAL`, `SCHEDULED`) |
| demandcontrolmaxpower | The maximum power when in `MANUAL` mode. Values between 40 and 100 are accepted in an increment of 5. In `SCHEDULED` demand control mode, this channel will be updated with the calculated maximum power based on the current active schedule. |
| demandcontrolschedule | A JSON string that contains the scheduled demand control settings. See below. |
For the BRP15B61:
@ -110,6 +113,135 @@ For the BRP15B61:
| zone7 | Turns zone 7 on/off for the air conditioning unit. |
| zone8 | Turns zone 8 on/off for the air conditioning unit. |
## Demand Control
Some units have a _demand control_ feature to limit the maximum power usage to a certain percentage.
This is set through the `demandcontrolmode` channel which accepts `OFF`, `MANUAL`, `SCHEDULED`, or `AUTO`.
When changing the mode from `MANUAL` to another mode, the maximum power setting will be saved in the Binding's memory and restored when switching the mode back to `MANUAL`.
Equally, when changing the mode from `SCHEDULED` to another mode, the current schedule will be saved in the Binding's memory and restored when switching the mode back to `SCHEDULED`.
### Manual Demand Control
Manual demand control requires setting the `demandcontrolmaxpower` channel to the desired limit.
The unit accepts values between 40% and 100% in increments of 5.
Sending a command to the `demandcontrolmaxpower` channel will automatically switch the demand control mode to `MANUAL`.
### Scheduled Demand Control
It is possible to set the demand control power limit based on day of the week and time of day schedules.
When the unit is in scheduled demand control mode, the binding provides the current schedule through the `demandcontrolschedule` channel.
In `SCHEDULED`, the `demandcontrolmaxpower` channel will provide the _current_ maximum power in effect, as defined within the schedule.
This information is not provided by the unit itself.
It is calculated by the binding based on the current schedule.
Therefore, it is important to ensure that openHAB's local time is in sync with the unit's date/time.
The schedule and associated max power settings can be set by sending a command to the `demandcontrolschedule` channel.
When doing so, the demand control mode will automatically change to `SCHEDULED`, if it wasn't already in that mode.
The schedule is specified in a JSON string in the following format:
```json
{
"monday": [
{
"enabled": true,
"time": <minutes from midnight>,
"power": <power in percent>
}
],
"tuesday": [
// Schedule entries for Tuesday
],
"wednesday": [
],
// more days up to Sunday
"sunday": [
]
}
```
Concrete example:
The JSON format doesn't actually support comments. They are provided for clarity.
```json
{
"monday": [
{
"enabled": true,
"time": 480, // 8 am
"power": 80
},
{
"enabled": true,
"time": 600, // 10 am
"power": 100
},
{
"enabled": true,
"time": 960, // 4pm
"power": 50
}
],
"tuesday": [
{
"enabled": true,
"time": 480, // 8 am
"power": 80
},
{
"enabled": true,
"time": 600, // 10 am
"power": 100
},
{
"enabled": true,
"time": 960, // 4pm
"power": 50
}
],
"wednesday": [
{
"enabled": true,
"time": 480, // 8 am
"power": 80
},
{
"enabled": true,
"time": 600, // 10 am
"power": 100
},
{
"enabled": true,
"time": 960, // 4pm
"power": 50
}
],
"thursday": [
{
"enabled": true,
"time": 480, // 8 am
"power": 100
}
]
// omitted days mean that they contain no schedules
}
```
Note:
- Each day can have up to 4 schedule entries
- `enabled` means whether this schedule element is enabled.
- `time` is the start time of the schedule, expressed in number of minutes from midnight.
- `power` a value of zero means demand power is disabled at the time defined by the `time` element.
- When there are no schedules defined for the current day/time, it is believed that the settings from the previous schedule will apply, bearing in mind that it is a weekly recurring schedule.
This is ultimately determined by the logic in the unit itself, and not controlled by the Binding.
## Full Example
daikin.things:
@ -135,7 +267,10 @@ String DaikinACUnit_Fan { channel="daikin:ac_unit:living_room_ac:fanspeed" }
String DaikinACUnit_Fan_Movement { channel="daikin:ac_unit:living_room_ac:fandir" }
Number:Temperature DaikinACUnit_IndoorTemperature { channel="daikin:ac_unit:living_room_ac:indoortemp" }
Number:Temperature DaikinACUnit_OutdoorTemperature { channel="daikin:ac_unit:living_room_ac:outdoortemp" }
// Demand control, when supported by the unit
String DaikinACUnit_DemandControl_Mode { channel="daikin:ac_unit:living_room_ac:demandcontrolmode" }
Dimmer DaikinACUnit_DemandControl_MaxPower { channel="daikin:ac_unit:living_room_ac:demandcontrolmaxpower" }
String DaikinACUnit_DemandControl_Schedule { channel="daikin:ac_unit:living_room_ac:demandcontrolschedule" }
// for Airbase (BRP15B61)
Switch DaikinACUnit_Power { channel="daikin:airbase_ac_unit:living_room_ac:power" }
@ -153,7 +288,6 @@ Switch DaikinACUnit_Zone5 { channel="daikin:airbase_ac_unit:living_room_ac:zone5
Switch DaikinACUnit_Zone6 { channel="daikin:airbase_ac_unit:living_room_ac:zone6" }
Switch DaikinACUnit_Zone7 { channel="daikin:airbase_ac_unit:living_room_ac:zone7" }
Switch DaikinACUnit_Zone8 { channel="daikin:airbase_ac_unit:living_room_ac:zone8" }
```
daikin.sitemap:
@ -183,5 +317,4 @@ Switch item=DaikinACUnit_Zone5 visibility=[DaikinACUnit_Power==ON]
Switch item=DaikinACUnit_Zone6 visibility=[DaikinACUnit_Power==ON]
Switch item=DaikinACUnit_Zone7 visibility=[DaikinACUnit_Power==ON]
Switch item=DaikinACUnit_Zone8 visibility=[DaikinACUnit_Power==ON]
```

View File

@ -64,6 +64,10 @@ public class DaikinBindingConstants {
public static final String CHANNEL_AC_SPECIALMODE = "specialmode";
public static final String CHANNEL_AC_STREAMER = "streamer";
public static final String CHANNEL_AC_DEMAND_MODE = "demandcontrolmode";
public static final String CHANNEL_AC_DEMAND_MAX_POWER = "demandcontrolmaxpower";
public static final String CHANNEL_AC_DEMAND_SCHEDULE = "demandcontrolschedule";
// additional channels for Airbase Controller
public static final String CHANNEL_AIRBASE_AC_FAN_SPEED = "airbasefanspeed";
public static final String CHANNEL_AIRBASE_AC_ZONE = "zone";

View File

@ -29,6 +29,7 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.daikin.internal.api.BasicInfo;
import org.openhab.binding.daikin.internal.api.ControlInfo;
import org.openhab.binding.daikin.internal.api.DemandControl;
import org.openhab.binding.daikin.internal.api.EnergyInfoDayAndWeek;
import org.openhab.binding.daikin.internal.api.EnergyInfoYear;
import org.openhab.binding.daikin.internal.api.Enums.SpecialMode;
@ -62,6 +63,8 @@ public class DaikinWebTargets {
private String getEnergyInfoYearUri;
private String getEnergyInfoWeekUri;
private String setSpecialModeUri;
private String setDemandControlUri;
private String getDemandControlUri;
private String setAirbaseControlInfoUri;
private String getAirbaseControlInfoUri;
@ -90,6 +93,8 @@ public class DaikinWebTargets {
getEnergyInfoYearUri = baseUri + "aircon/get_year_power_ex";
getEnergyInfoWeekUri = baseUri + "aircon/get_week_power_ex";
setSpecialModeUri = baseUri + "aircon/set_special_mode";
setDemandControlUri = baseUri + "aircon/set_demand_control";
getDemandControlUri = baseUri + "aircon/get_demand_control";
// Daikin Airbase API
getAirbaseBasicInfoUri = baseUri + "skyfi/common/basic_info";
@ -169,6 +174,18 @@ public class DaikinWebTargets {
}
}
public DemandControl getDemandControl() throws DaikinCommunicationException {
String response = invoke(getDemandControlUri);
return DemandControl.parse(response);
}
public boolean setDemandControl(DemandControl info) throws DaikinCommunicationException {
Map<String, String> queryParams = info.getParamString();
String result = invoke(setDemandControlUri, queryParams);
Map<String, String> responseMap = InfoParser.parse(result);
return Optional.ofNullable(responseMap.get("ret")).orElse("").equals("OK");
}
// Daikin Airbase API
public AirbaseControlInfo getAirbaseControlInfo() throws DaikinCommunicationException {
String response = invoke(getAirbaseControlInfoUri);

View File

@ -0,0 +1,187 @@
/**
* 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.daikin.internal.api;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.daikin.internal.api.Enums.DemandControlMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
/**
* Class for holding the set of parameters used by set and get demand control info.
*
* @author Jimmy Tanagra - Initial Contribution
*
*/
@NonNullByDefault
public class DemandControl {
private static final Logger LOGGER = LoggerFactory.getLogger(DemandControl.class);
private static final List<String> DAYS = List.of("monday", "tuesday", "wednesday", "thursday", "friday", "saturday",
"sunday");
// create a map of "monday" -> "mo", "tuesday" -> "tu", etc.
private static final Map<String, String> DAYS_ABBREVIATIONS = DAYS.stream()
.map(day -> Map.entry(day, day.substring(0, 2)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
private static Gson GSON = new Gson();
public String ret = "";
public DemandControlMode mode = DemandControlMode.AUTO;
public int maxPower = 100;
private Map<String, List<ScheduleEntry>> scheduleMap = new HashMap<>();
private DemandControl() {
}
public String getSchedule() {
return GSON.toJson(scheduleMap);
}
public void setSchedule(String schedule) throws JsonSyntaxException {
Map<String, List<ScheduleEntry>> parsedMap = GSON.fromJson(schedule,
new TypeToken<Map<String, List<ScheduleEntry>>>() {
}.getType());
if (DAYS.containsAll(parsedMap.keySet())) {
scheduleMap = parsedMap;
} else {
throw new JsonSyntaxException("Invalid day(s) in JSON data");
}
}
public int getScheduledMaxPower() {
return getScheduledMaxPower(LocalDateTime.now());
}
// Returns the current max_power setting based on the schedule
// If there are no matching schedules for the current time,
// it will search the last schedule of the previous non-empty day
public int getScheduledMaxPower(LocalDateTime dateTime) {
int todayIndex = dateTime.getDayOfWeek().getValue() - 1;
String today = DAYS.get(todayIndex);
int currentMinsFromMidnight = dateTime.toLocalTime().toSecondOfDay() / 60;
// search today's schedule for the last applicable schedule
Optional<Integer> maxPower = scheduleMap.get(today).stream().filter(entry -> entry.enabled)
.sorted((s1, s2) -> Integer.compare(s1.time, s2.time))
.takeWhile(scheduleEntry -> scheduleEntry.time <= currentMinsFromMidnight)
.reduce((first, second) -> second) // get the last entry that matches the condition
.map(scheduleEntry -> scheduleEntry.power).or(() -> {
// there are no matching schedules today, so
// get the last entry of the previous non-empty schedule day,
// wrapping around the DAYS array if necessary
int currentIndex = todayIndex > 0 ? (todayIndex - 1) : (DAYS.size() - 1);
while (currentIndex != todayIndex) {
String prevDay = DAYS.get(currentIndex);
List<ScheduleEntry> prevDaySchedules = scheduleMap.get(prevDay).stream()
.filter(entry -> entry.enabled).sorted((s1, s2) -> Integer.compare(s1.time, s2.time))
.toList();
if (!prevDaySchedules.isEmpty()) {
return Optional.of(prevDaySchedules.get(prevDaySchedules.size() - 1).power);
}
currentIndex = currentIndex > 0 ? (currentIndex - 1) : (DAYS.size() - 1);
}
// if previous days have no schedules, use today's last schedule if any
return scheduleMap.get(today).stream().filter(entry -> entry.enabled)
.sorted((s1, s2) -> Integer.compare(s1.time, s2.time)).reduce((first, second) -> second)
.map(scheduleEntry -> scheduleEntry.power);
});
return maxPower.map(value -> value == 0 ? 100 : value) // a maxPower of 0 means the demand control is disabled,
// so return 100
.orElse(100); // return 100 also for no schedules
}
public static DemandControl parse(String response) {
LOGGER.trace("Parsing string: \"{}\"", response);
Map<String, String> responseMap = InfoParser.parse(response);
DemandControl info = new DemandControl();
info.ret = responseMap.getOrDefault("ret", "");
boolean enabled = "1".equals(responseMap.get("en_demand"));
if (!enabled) {
info.mode = DemandControlMode.OFF;
} else {
info.mode = DemandControlMode.fromValue(responseMap.getOrDefault("mode", "-"));
}
info.maxPower = Objects.requireNonNull(Optional.ofNullable(responseMap.get("max_pow"))
.flatMap(value -> InfoParser.parseInt(value)).orElse(100));
info.scheduleMap = DAYS_ABBREVIATIONS.entrySet().stream().map(day -> {
final String dayName = day.getKey();
final String dayPrefix = day.getValue();
final int dayCount = Objects.requireNonNull(Optional.ofNullable(responseMap.get(dayPrefix + "c"))
.flatMap(value -> InfoParser.parseInt(value)).orElse(0));
// We don't want to sort the entries by time here, to preserve the same order from the response
List<ScheduleEntry> schedules = Stream.iterate(1, i -> i <= dayCount, i -> i + 1).map(i -> {
String prefix = dayPrefix + i + "_";
return new ScheduleEntry("1".equals(responseMap.get(prefix + "en")),
Objects.requireNonNull(Optional.ofNullable(responseMap.get(prefix + "t"))
.flatMap(value -> InfoParser.parseInt(value)).orElse(0)),
Objects.requireNonNull(Optional.ofNullable(responseMap.get(prefix + "p"))
.flatMap(value -> InfoParser.parseInt(value)).orElse(0)));
}).toList();
return Map.entry(dayName, schedules);
}).collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));
return info;
}
public Map<String, String> getParamString() {
Map<String, String> params = new HashMap<>();
params.put("en_demand", mode == DemandControlMode.OFF ? "0" : "1");
if (mode != DemandControlMode.OFF) {
params.put("mode", mode.getValue());
params.put("max_pow", Integer.toString(maxPower));
DAYS.stream().forEach(day -> {
String dayPrefix = DAYS_ABBREVIATIONS.get(day);
List<ScheduleEntry> schedules = scheduleMap.getOrDefault(day, List.of());
params.put(dayPrefix + "c", Integer.toString(schedules.size()));
for (int i = 0; i < schedules.size(); i++) {
ScheduleEntry schedule = schedules.get(i);
String prefix = dayPrefix + (i + 1) + "_";
params.put(prefix + "en", schedule.enabled ? "1" : "0");
params.put(prefix + "t", Integer.toString(schedule.time));
params.put(prefix + "p", Integer.toString(schedule.power));
}
});
}
return params;
}
// package private for testing
record ScheduleEntry(boolean enabled, int time, int power) {
}
}

View File

@ -209,4 +209,43 @@ public class Enums {
return NORMAL;
}
}
public enum DemandControlMode {
OFF("-"),
MANUAL("0"),
SCHEDULED("1"),
AUTO("2");
private final String value;
private static final Logger LOGGER = LoggerFactory.getLogger(DemandControlMode.class);
DemandControlMode(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public static boolean isValidValue(String value) {
for (DemandControlMode m : DemandControlMode.values()) {
if (m.getValue().equals(value)) {
return true;
}
}
return false;
}
public static DemandControlMode fromValue(String value) {
for (DemandControlMode m : DemandControlMode.values()) {
if (m.getValue().equals(value)) {
return m;
}
}
LOGGER.debug("Unexpected DemandControlMode value of \"{}\"", value);
// Default to off
return OFF;
}
}
}

View File

@ -24,8 +24,10 @@ import org.openhab.binding.daikin.internal.DaikinBindingConstants;
import org.openhab.binding.daikin.internal.DaikinCommunicationException;
import org.openhab.binding.daikin.internal.DaikinDynamicStateDescriptionProvider;
import org.openhab.binding.daikin.internal.api.ControlInfo;
import org.openhab.binding.daikin.internal.api.DemandControl;
import org.openhab.binding.daikin.internal.api.EnergyInfoDayAndWeek;
import org.openhab.binding.daikin.internal.api.EnergyInfoYear;
import org.openhab.binding.daikin.internal.api.Enums.DemandControlMode;
import org.openhab.binding.daikin.internal.api.Enums.FanMovement;
import org.openhab.binding.daikin.internal.api.Enums.FanSpeed;
import org.openhab.binding.daikin.internal.api.Enums.HomekitMode;
@ -34,6 +36,7 @@ import org.openhab.binding.daikin.internal.api.Enums.SpecialMode;
import org.openhab.binding.daikin.internal.api.SensorInfo;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
@ -45,6 +48,8 @@ import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* Handles communicating with a Daikin air conditioning unit.
*
@ -59,6 +64,9 @@ public class DaikinAcUnitHandler extends DaikinBaseHandler {
private final Logger logger = LoggerFactory.getLogger(DaikinAcUnitHandler.class);
private Optional<Integer> autoModeValue = Optional.empty();
private boolean pollDemandControl = true;
private Optional<String> savedDemandControlSchedule = Optional.empty();
private Optional<Integer> savedDemandControlMaxPower = Optional.empty();
public DaikinAcUnitHandler(Thing thing, DaikinDynamicStateDescriptionProvider stateDescriptionProvider,
@Nullable HttpClient httpClient) {
@ -153,6 +161,29 @@ public class DaikinAcUnitHandler extends DaikinBaseHandler {
// Suppress any error if energy info is not supported.
logger.debug("getEnergyInfoDayAndWeek() error: {}", e.getMessage());
}
if (pollDemandControl) {
try {
DemandControl demandInfo = webTargets.getDemandControl();
String schedule = demandInfo.getSchedule();
int maxPower = demandInfo.maxPower;
if (demandInfo.mode == DemandControlMode.SCHEDULED) {
savedDemandControlSchedule = Optional.of(schedule);
maxPower = demandInfo.getScheduledMaxPower();
} else if (demandInfo.mode == DemandControlMode.MANUAL) {
savedDemandControlMaxPower = Optional.of(demandInfo.maxPower);
}
updateState(DaikinBindingConstants.CHANNEL_AC_DEMAND_MODE, new StringType(demandInfo.mode.name()));
updateState(DaikinBindingConstants.CHANNEL_AC_DEMAND_MAX_POWER, new PercentType(maxPower));
updateState(DaikinBindingConstants.CHANNEL_AC_DEMAND_SCHEDULE, new StringType(schedule));
} catch (DaikinCommunicationException e) {
// Suppress any error if demand control is not supported.
logger.debug("getDemandControl() error: {}", e.getMessage());
pollDemandControl = false;
}
}
}
@Override
@ -177,6 +208,24 @@ public class DaikinAcUnitHandler extends DaikinBaseHandler {
return true;
}
break;
case DaikinBindingConstants.CHANNEL_AC_DEMAND_MODE:
if (command instanceof StringType stringCommand) {
changeDemandMode(stringCommand.toString());
return true;
}
break;
case DaikinBindingConstants.CHANNEL_AC_DEMAND_MAX_POWER:
if (command instanceof PercentType percentCommand) {
changeDemandMaxPower(percentCommand.intValue());
return true;
}
break;
case DaikinBindingConstants.CHANNEL_AC_DEMAND_SCHEDULE:
if (command instanceof StringType stringCommand) {
changeDemandSchedule(stringCommand.toString());
return true;
}
break;
}
return false;
}
@ -265,6 +314,51 @@ public class DaikinAcUnitHandler extends DaikinBaseHandler {
webTargets.setStreamerMode(streamerMode);
}
protected void changeDemandMode(String mode) throws DaikinCommunicationException {
DemandControlMode newMode;
try {
newMode = DemandControlMode.valueOf(mode);
} catch (IllegalArgumentException e) {
logger.warn("Invalid demand mode: {}. Valid values: {}", mode, DemandControlMode.values());
return;
}
DemandControl demandInfo = webTargets.getDemandControl();
if (demandInfo.mode != newMode) {
if (newMode == DemandControlMode.SCHEDULED && savedDemandControlSchedule.isPresent()) {
// restore previously saved schedule
demandInfo.setSchedule(savedDemandControlSchedule.get());
}
if (newMode == DemandControlMode.MANUAL && savedDemandControlMaxPower.isPresent()) {
// restore previously saved maxPower
demandInfo.maxPower = savedDemandControlMaxPower.get();
}
}
demandInfo.mode = newMode;
webTargets.setDemandControl(demandInfo);
}
protected void changeDemandMaxPower(int maxPower) throws DaikinCommunicationException {
DemandControl demandInfo = webTargets.getDemandControl();
demandInfo.mode = DemandControlMode.MANUAL;
demandInfo.maxPower = maxPower;
webTargets.setDemandControl(demandInfo);
savedDemandControlMaxPower = Optional.of(maxPower);
}
protected void changeDemandSchedule(String schedule) throws DaikinCommunicationException {
DemandControl demandInfo = webTargets.getDemandControl();
try {
demandInfo.setSchedule(schedule);
} catch (JsonSyntaxException e) {
logger.warn("Invalid schedule: {}. {}", schedule, e.getMessage());
return;
}
demandInfo.mode = DemandControlMode.SCHEDULED;
webTargets.setDemandControl(demandInfo);
savedDemandControlSchedule = Optional.of(demandInfo.getSchedule());
}
/**
* Updates energy year channels. Values are provided in hundreds of Watt
*

View File

@ -27,8 +27,24 @@ thing-type.config.daikin.config.uuid.description = A unique UUID for authenticat
channel-type.daikin.acunit-cmpfrequency.label = Compressor Frequency
channel-type.daikin.acunit-cmpfrequency.description = Current compressor frequency
channel-type.daikin.acunit-demandcontrolmaxpower.label = Demand Control Max Power
channel-type.daikin.acunit-demandcontrolmaxpower.description = The maximum power for demand control in percent. Allowed range is between 40% and 100% in increments of 5%.
channel-type.daikin.acunit-demandcontrolmode.label = Demand Control Mode
channel-type.daikin.acunit-demandcontrolmode.description = The demand control mode
channel-type.daikin.acunit-demandcontrolmode.state.option.OFF = Off
channel-type.daikin.acunit-demandcontrolmode.state.option.AUTO = Auto
channel-type.daikin.acunit-demandcontrolmode.state.option.SCHEDULED = Scheduled
channel-type.daikin.acunit-demandcontrolmode.state.option.MANUAL = Manual
channel-type.daikin.acunit-demandcontrolschedule.label = Demand Control Schedule
channel-type.daikin.acunit-demandcontrolschedule.description = The demand control schedule in JSON format.
channel-type.daikin.acunit-energycoolingcurrentyear-1.label = Energy Cooling Current Year January
channel-type.daikin.acunit-energycoolingcurrentyear-1.description = The energy usage for cooling this year January
channel-type.daikin.acunit-energycoolingcurrentyear-10.label = Energy Cooling Current Year October
channel-type.daikin.acunit-energycoolingcurrentyear-10.description = The energy usage for cooling this year October
channel-type.daikin.acunit-energycoolingcurrentyear-11.label = Energy Cooling Current Year November
channel-type.daikin.acunit-energycoolingcurrentyear-11.description = The energy usage for cooling this year November
channel-type.daikin.acunit-energycoolingcurrentyear-12.label = Energy Cooling Current Year December
channel-type.daikin.acunit-energycoolingcurrentyear-12.description = The energy usage for cooling this year December
channel-type.daikin.acunit-energycoolingcurrentyear-2.label = Energy Cooling Current Year February
channel-type.daikin.acunit-energycoolingcurrentyear-2.description = The energy usage for cooling this year February
channel-type.daikin.acunit-energycoolingcurrentyear-3.label = Energy Cooling Current Year March
@ -45,12 +61,6 @@ channel-type.daikin.acunit-energycoolingcurrentyear-8.label = Energy Cooling Cur
channel-type.daikin.acunit-energycoolingcurrentyear-8.description = The energy usage for cooling this year August
channel-type.daikin.acunit-energycoolingcurrentyear-9.label = Energy Cooling Current Year September
channel-type.daikin.acunit-energycoolingcurrentyear-9.description = The energy usage for cooling this year September
channel-type.daikin.acunit-energycoolingcurrentyear-10.label = Energy Cooling Current Year October
channel-type.daikin.acunit-energycoolingcurrentyear-10.description = The energy usage for cooling this year October
channel-type.daikin.acunit-energycoolingcurrentyear-11.label = Energy Cooling Current Year November
channel-type.daikin.acunit-energycoolingcurrentyear-11.description = The energy usage for cooling this year November
channel-type.daikin.acunit-energycoolingcurrentyear-12.label = Energy Cooling Current Year December
channel-type.daikin.acunit-energycoolingcurrentyear-12.description = The energy usage for cooling this year December
channel-type.daikin.acunit-energycoolinglastweek.label = Energy Cooling Last Week
channel-type.daikin.acunit-energycoolinglastweek.description = The energy usage for cooling last week
channel-type.daikin.acunit-energycoolingthisweek.label = Energy Cooling This Week
@ -59,6 +69,12 @@ channel-type.daikin.acunit-energycoolingtoday.label = Energy Cooling Today
channel-type.daikin.acunit-energycoolingtoday.description = The energy usage for cooling today
channel-type.daikin.acunit-energyheatingcurrentyear-1.label = Energy Heating Current Year January
channel-type.daikin.acunit-energyheatingcurrentyear-1.description = The energy usage for heating this year January
channel-type.daikin.acunit-energyheatingcurrentyear-10.label = Energy Heating Current Year October
channel-type.daikin.acunit-energyheatingcurrentyear-10.description = The energy usage for heating this year October
channel-type.daikin.acunit-energyheatingcurrentyear-11.label = Energy Heating Current Year November
channel-type.daikin.acunit-energyheatingcurrentyear-11.description = The energy usage for heating this year November
channel-type.daikin.acunit-energyheatingcurrentyear-12.label = Energy Heating Current Year December
channel-type.daikin.acunit-energyheatingcurrentyear-12.description = The energy usage for heating this year December
channel-type.daikin.acunit-energyheatingcurrentyear-2.label = Energy Heating Current Year February
channel-type.daikin.acunit-energyheatingcurrentyear-2.description = The energy usage for heating this year February
channel-type.daikin.acunit-energyheatingcurrentyear-3.label = Energy Heating Current Year March
@ -75,12 +91,6 @@ channel-type.daikin.acunit-energyheatingcurrentyear-8.label = Energy Heating Cur
channel-type.daikin.acunit-energyheatingcurrentyear-8.description = The energy usage for heating this year August
channel-type.daikin.acunit-energyheatingcurrentyear-9.label = Energy Heating Current Year September
channel-type.daikin.acunit-energyheatingcurrentyear-9.description = The energy usage for heating this year September
channel-type.daikin.acunit-energyheatingcurrentyear-10.label = Energy Heating Current Year October
channel-type.daikin.acunit-energyheatingcurrentyear-10.description = The energy usage for heating this year October
channel-type.daikin.acunit-energyheatingcurrentyear-11.label = Energy Heating Current Year November
channel-type.daikin.acunit-energyheatingcurrentyear-11.description = The energy usage for heating this year November
channel-type.daikin.acunit-energyheatingcurrentyear-12.label = Energy Heating Current Year December
channel-type.daikin.acunit-energyheatingcurrentyear-12.description = The energy usage for heating this year December
channel-type.daikin.acunit-energyheatinglastweek.label = Energy Heating Last Week
channel-type.daikin.acunit-energyheatinglastweek.description = The energy usage for heating last week
channel-type.daikin.acunit-energyheatingthisweek.label = Energy Heating This Week

View File

@ -51,8 +51,14 @@
<channel id="energycoolingcurrentyear-10" typeId="acunit-energycoolingcurrentyear-10"></channel>
<channel id="energycoolingcurrentyear-11" typeId="acunit-energycoolingcurrentyear-11"></channel>
<channel id="energycoolingcurrentyear-12" typeId="acunit-energycoolingcurrentyear-12"></channel>
<channel id="demandcontrolmode" typeId="acunit-demandcontrolmode"></channel>
<channel id="demandcontrolmaxpower" typeId="acunit-demandcontrolmaxpower"></channel>
<channel id="demandcontrolschedule" typeId="acunit-demandcontrolschedule"></channel>
</channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>host</representation-property>
<config-description-ref uri="thing-type:daikin:config"/>
</thing-type>
@ -424,6 +430,32 @@
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="acunit-demandcontrolmode" advanced="true">
<item-type>String</item-type>
<label>Demand Control Mode</label>
<description>The demand control mode</description>
<state>
<options>
<option value="OFF">Off</option>
<option value="AUTO">Auto</option>
<option value="SCHEDULED">Scheduled</option>
<option value="MANUAL">Manual</option>
</options>
</state>
</channel-type>
<channel-type id="acunit-demandcontrolmaxpower" advanced="true">
<item-type>Dimmer</item-type>
<label>Demand Control Max Power</label>
<description>The maximum power for demand control in percent. Allowed range is between 40% and 100% in increments of
5%.</description>
<state pattern="%d %%" min="40" max="100" step="5"></state>
</channel-type>
<channel-type id="acunit-demandcontrolschedule" advanced="true">
<item-type>String</item-type>
<label>Demand Control Schedule</label>
<description>The demand control schedule in JSON format.</description>
</channel-type>
<channel-type id="airbase-acunit-fan">
<item-type>String</item-type>
<label>Fan</label>

View File

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

View File

@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test;
import org.openhab.binding.daikin.internal.api.Enums.FanMovement;
/**
* This class provides tests for deconz lights
* This class provides tests for the ControlInfo class
*
* @author Leo Siepel - Initial contribution
*

View File

@ -0,0 +1,388 @@
/**
* 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.daikin.internal.api;
import static java.time.DayOfWeek.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.binding.daikin.internal.api.DemandControl.ScheduleEntry;
import org.openhab.binding.daikin.internal.api.Enums.DemandControlMode;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
/**
* This class provides tests for the DemandControl class
*
* @author Jimmy Tanagra - Initial contribution
*
*/
@NonNullByDefault
public class DemandControlTest {
public static Stream<Arguments> parserTest() {
return Stream.of( //
Arguments.of("ret=OK,type=1,en_demand=0,mode=0,max_pow=100", DemandControlMode.OFF, 100),
Arguments.of("ret=OK,type=1,en_demand=1,mode=0,max_pow=100", DemandControlMode.MANUAL, 100),
Arguments.of("ret=OK,type=1,en_demand=0,mode=1,max_pow=100", DemandControlMode.OFF, 100),
Arguments.of("ret=OK,type=1,en_demand=1,mode=1,max_pow=100", DemandControlMode.SCHEDULED, 100),
Arguments.of("ret=OK,type=1,en_demand=0,mode=2,max_pow=100", DemandControlMode.OFF, 100),
Arguments.of("ret=OK,type=1,en_demand=1,mode=2,max_pow=100", DemandControlMode.AUTO, 100),
Arguments.of("ret=OK,type=1,en_demand=0,mode=0,max_pow=50", DemandControlMode.OFF, 50),
Arguments.of("ret=OK,type=1,en_demand=0,mode=0,max_pow=40", DemandControlMode.OFF, 40),
// Invalid inputs - defaults
Arguments.of("ret=OK,type=1,en_demand=,mode=,max_pow=", DemandControlMode.OFF, 100)
//
);
}
@ParameterizedTest
@MethodSource
public void parserTest(String input, DemandControlMode expectedMode, int expectedMaxPower) {
DemandControl info = DemandControl.parse(input);
// assert
assertEquals(expectedMode, info.mode);
assertEquals(expectedMaxPower, info.maxPower);
}
public static Stream<Arguments> inputScheduleParserTest() {
return Stream.of( //
Arguments.of(
"ret=OK,type=1,en_demand=0,mode=0,max_pow=100,scdl_per_day=4,moc=0,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
Map.of("monday", List.of(), "tuesday", List.of(), "wednesday", List.of(), "thursday", List.of(),
"friday", List.of(), "saturday", List.of(), "sunday", List.of())),
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,moc=3,mo1_en=1,mo1_t=720,mo1_p=90,mo2_en=1,mo2_t=840,mo2_p=0,mo3_en=1,mo3_t=600,mo3_p=70,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
Map.of("monday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70)),
"tuesday", List.of(), "wednesday", List.of(), "thursday", List.of(), "friday",
List.of(), "saturday", List.of(), "sunday", List.of())),
// added mo4_xxx but moc=3
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,moc=3,mo1_en=1,mo1_t=720,mo1_p=90,mo2_en=1,mo2_t=840,mo2_p=0,mo3_en=1,mo3_t=600,mo3_p=70,mo4_en=0,mo4_t=30,mo4_p=0",
Map.of("monday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70)),
"tuesday", List.of(), "wednesday", List.of(), "thursday", List.of(), "friday",
List.of(), "saturday", List.of(), "sunday", List.of())),
// this time moc=4
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,moc=4,mo1_en=1,mo1_t=720,mo1_p=90,mo2_en=1,mo2_t=840,mo2_p=0,mo3_en=1,mo3_t=600,mo3_p=70,mo4_en=0,mo4_t=30,mo4_p=0",
Map.of("monday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70), new ScheduleEntry(false, 30, 0)),
"tuesday", List.of(), "wednesday", List.of(), "thursday", List.of(), "friday",
List.of(), "saturday", List.of(), "sunday", List.of())),
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,tuc=4,tu1_en=1,tu1_t=720,tu1_p=90,tu2_en=1,tu2_t=840,tu2_p=0,tu3_en=1,tu3_t=600,tu3_p=70,tu4_en=0,tu4_t=30,tu4_p=0",
Map.of("tuesday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70), new ScheduleEntry(false, 30, 0)),
"monday", List.of(), "wednesday", List.of(), "thursday", List.of(), "friday", List.of(),
"saturday", List.of(), "sunday", List.of())),
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,wec=4,we1_en=1,we1_t=720,we1_p=90,we2_en=1,we2_t=840,we2_p=0,we3_en=1,we3_t=600,we3_p=70,we4_en=0,we4_t=30,we4_p=0",
Map.of("wednesday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70), new ScheduleEntry(false, 30, 0)),
"monday", List.of(), "tuesday", List.of(), "thursday", List.of(), "friday", List.of(),
"saturday", List.of(), "sunday", List.of())),
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,thc=4,th1_en=1,th1_t=720,th1_p=90,th2_en=1,th2_t=840,th2_p=0,th3_en=1,th3_t=600,th3_p=70,th4_en=0,th4_t=30,th4_p=0",
Map.of("thursday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70), new ScheduleEntry(false, 30, 0)),
"monday", List.of(), "tuesday", List.of(), "wednesday", List.of(), "friday", List.of(),
"saturday", List.of(), "sunday", List.of())),
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,frc=4,fr1_en=1,fr1_t=720,fr1_p=90,fr2_en=1,fr2_t=840,fr2_p=0,fr3_en=1,fr3_t=600,fr3_p=70,fr4_en=0,fr4_t=30,fr4_p=0",
Map.of("friday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70), new ScheduleEntry(false, 30, 0)),
"monday", List.of(), "tuesday", List.of(), "wednesday", List.of(), "thursday",
List.of(), "saturday", List.of(), "sunday", List.of())),
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,sac=4,sa1_en=1,sa1_t=720,sa1_p=90,sa2_en=1,sa2_t=840,sa2_p=0,sa3_en=1,sa3_t=600,sa3_p=70,sa4_en=0,sa4_t=30,sa4_p=0",
Map.of("saturday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70), new ScheduleEntry(false, 30, 0)),
"monday", List.of(), "tuesday", List.of(), "wednesday", List.of(), "thursday",
List.of(), "friday", List.of(), "sunday", List.of())),
Arguments.of(
"ret=OK,type=1,en_demand=1,mode=1,max_pow=100,scdl_per_day=4,suc=4,su1_en=1,su1_t=720,su1_p=90,su2_en=1,su2_t=840,su2_p=0,su3_en=1,su3_t=600,su3_p=70,su4_en=0,su4_t=30,su4_p=0",
Map.of("sunday",
List.of(new ScheduleEntry(true, 720, 90), new ScheduleEntry(true, 840, 0),
new ScheduleEntry(true, 600, 70), new ScheduleEntry(false, 30, 0)),
"monday", List.of(), "tuesday", List.of(), "wednesday", List.of(), "thursday",
List.of(), "friday", List.of(), "saturday", List.of()))
//
);
}
@ParameterizedTest
@MethodSource
public void inputScheduleParserTest(String input, Map<String, List<ScheduleEntry>> expectedSchedule) {
DemandControl info = DemandControl.parse(input);
var parsedJsonObject = parseJson(info.getSchedule());
String expectedJsonString = new Gson().toJson(expectedSchedule);
var expectedJsonObject = parseJson(expectedJsonString);
// assert
assertEquals(expectedJsonObject, parsedJsonObject);
}
public static Stream<Arguments> jsonScheduleToParamStringTest() {
return Stream.of( //
Arguments.of( //
"""
{
"monday": [
{"enabled":true,"time":720,"power":90},
{"enabled":true,"time":840,"power":0},
{"enabled":false,"time":600,"power":70},
{"enabled":true,"time":300,"power":50}
],
"tuesday":[],"wednesday":[],"thursday":[],"friday":[],"saturday":[],"sunday":[]
}
""", //
"moc=4," + //
"mo1_en=1,mo1_t=720,mo1_p=90," + //
"mo2_en=1,mo2_t=840,mo2_p=0," + //
"mo3_en=0,mo3_t=600,mo3_p=70," + //
"mo4_en=1,mo4_t=300,mo4_p=50," + //
"tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0" //
), //
Arguments.of( //
"""
{
"tuesday": [
{"enabled":true,"time":720,"power":90},
{"enabled":true,"time":840,"power":0},
{"enabled":false,"time":600,"power":70},
{"enabled":true,"time":300,"power":50}
],
"monday":[],"wednesday":[],"thursday":[],"friday":[],"saturday":[],"sunday":[]
}
""", //
"tuc=4," + //
"tu1_en=1,tu1_t=720,tu1_p=90," + //
"tu2_en=1,tu2_t=840,tu2_p=0," + //
"tu3_en=0,tu3_t=600,tu3_p=70," + //
"tu4_en=1,tu4_t=300,tu4_p=50," + //
"moc=0,wec=0,thc=0,frc=0,sac=0,suc=0" //
), //
Arguments.of( //
"""
{
"wednesday": [
{"enabled":true,"time":720,"power":90},
{"enabled":true,"time":840,"power":0},
{"enabled":false,"time":600,"power":70},
{"enabled":true,"time":300,"power":50}
],
"monday":[],"tuesday":[],"thursday":[],"friday":[],"saturday":[],"sunday":[]
}
""", //
"wec=4," + //
"we1_en=1,we1_t=720,we1_p=90," + //
"we2_en=1,we2_t=840,we2_p=0," + //
"we3_en=0,we3_t=600,we3_p=70," + //
"we4_en=1,we4_t=300,we4_p=50," + //
"moc=0,tuc=0,thc=0,frc=0,sac=0,suc=0" //
), //
Arguments.of( //
"""
{
"thursday": [
{"enabled":true,"time":720,"power":90},
{"enabled":true,"time":840,"power":0},
{"enabled":false,"time":600,"power":70},
{"enabled":true,"time":300,"power":50}
],
"monday":[],"tuesday":[],"wednesday":[],"friday":[],"saturday":[],"sunday":[]
}
""", //
"thc=4," + //
"th1_en=1,th1_t=720,th1_p=90," + //
"th2_en=1,th2_t=840,th2_p=0," + //
"th3_en=0,th3_t=600,th3_p=70," + //
"th4_en=1,th4_t=300,th4_p=50," + //
"moc=0,tuc=0,wec=0,frc=0,sac=0,suc=0" //
), //
Arguments.of( //
"""
{
"friday": [
{"enabled":true,"time":720,"power":90},
{"enabled":true,"time":840,"power":0},
{"enabled":false,"time":600,"power":70},
{"enabled":true,"time":300,"power":50}
],
"monday":[],"tuesday":[],"wednesday":[],"thursday":[],"saturday":[],"sunday":[]
}
""", //
"frc=4," + //
"fr1_en=1,fr1_t=720,fr1_p=90," + //
"fr2_en=1,fr2_t=840,fr2_p=0," + //
"fr3_en=0,fr3_t=600,fr3_p=70," + //
"fr4_en=1,fr4_t=300,fr4_p=50," + //
"moc=0,tuc=0,thc=0,wec=0,sac=0,suc=0" //
), //
Arguments.of( //
"""
{
"saturday": [
{"enabled":true,"time":720,"power":90},
{"enabled":true,"time":840,"power":0},
{"enabled":false,"time":600,"power":70},
{"enabled":true,"time":300,"power":50}
],
"monday":[],"tuesday":[],"wednesday":[],"thursday":[],"friday":[],"sunday":[]
}
""", //
"sac=4," + //
"sa1_en=1,sa1_t=720,sa1_p=90," + //
"sa2_en=1,sa2_t=840,sa2_p=0," + //
"sa3_en=0,sa3_t=600,sa3_p=70," + //
"sa4_en=1,sa4_t=300,sa4_p=50," + //
"moc=0,tuc=0,thc=0,frc=0,wec=0,suc=0" //
), //
Arguments.of( //
"""
{
"sunday": [
{"enabled":true,"time":720,"power":90},
{"enabled":true,"time":840,"power":0},
{"enabled":false,"time":600,"power":70},
{"enabled":true,"time":300,"power":50}
],
"monday":[],"tuesday":[],"thursday":[],"friday":[],"saturday":[],"wednesday":[]
}
""", //
"suc=4," + //
"su1_en=1,su1_t=720,su1_p=90," + //
"su2_en=1,su2_t=840,su2_p=0," + //
"su3_en=0,su3_t=600,su3_p=70," + //
"su4_en=1,su4_t=300,su4_p=50," + //
"moc=0,tuc=0,thc=0,frc=0,sac=0,wec=0" //
)//
);
}
@ParameterizedTest
@MethodSource
public void jsonScheduleToParamStringTest(String scheduleJson, String expectedParamString) {
DemandControl info = DemandControl.parse("ret=OK,type=1,en_demand=1,mode=1");
Map<String, String> expectedParamMap = InfoParser.parse(expectedParamString);
info.setSchedule(scheduleJson);
Map<String, String> paramMap = info.getParamString();
expectedParamMap.entrySet().stream().forEach(expectedParam -> assertThat(paramMap,
hasEntry(is(expectedParam.getKey()), is(expectedParam.getValue()))));
}
private @Nullable Map<String, List<ScheduleEntry>> parseJson(String json) {
return new Gson().fromJson(json, new TypeToken<Map<String, List<ScheduleEntry>>>() {
}.getType());
}
public static Stream<Arguments> scheduledMaxPowerTest() {
return Stream.of( //
// empty schedule
Arguments.of("moc=0,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0", MONDAY, "12:00", 100),
// within the schedule of the day
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=1,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
MONDAY, "10:00", 60),
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=1,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
MONDAY, "10:05", 60),
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=1,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
MONDAY, "12:00", 70),
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=1,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
MONDAY, "15:00", 80),
// it should ignore disabled schedules
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=0,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
MONDAY, "15:00", 70),
// earlier than first schedule of the day, must look back and find the last schedule
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=1,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=1,fr1_en=1,fr1_t=10,fr1_p=77,sac=0,suc=0",
MONDAY, "08:00", 77),
// test for boundary conditions (last item on the list, ie. sunday)
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=1,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=1,su1_en=1,su1_t=10,su1_p=77",
MONDAY, "08:00", 77),
// earlier than first schedule of the day, no other days have schedules,
// so wrap around and pick the last schedule of the same day
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=1,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
MONDAY, "08:00", 80),
// but also ignore disabled schedules
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=0,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
MONDAY, "08:00", 70),
// empty schedule for the day, so look back until we find the last schedule
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=1,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
WEDNESDAY, "15:00", 80),
// it should also ignore disabled schedules in the previous days
Arguments.of(
"moc=3,mo1_en=1,mo1_t=720,mo1_p=70,mo2_en=0,mo2_t=840,mo2_p=80,mo3_en=1,mo3_t=600,mo3_p=60,tuc=0,wec=0,thc=0,frc=0,sac=0,suc=0",
WEDNESDAY, "15:00", 70),
// it should wrap around and start search from the end of the week
Arguments.of(
"moc=0,tuc=3,tu1_en=1,tu1_t=720,tu1_p=70,tu2_en=1,tu2_t=840,tu2_p=80,tu3_en=1,tu3_t=600,tu3_p=60,wec=0,thc=0,frc=0,sac=0,suc=0",
MONDAY, "15:00", 80)
//
);
}
@ParameterizedTest
@MethodSource
public void scheduledMaxPowerTest(String input, DayOfWeek dow, String time, int expectedMaxPower) {
DemandControl info = DemandControl.parse(input);
LocalDateTime dateTime = LocalDateTime.now().with(java.time.temporal.TemporalAdjusters.next(dow))
.with(java.time.LocalTime.parse(time));
assertEquals(expectedMaxPower, info.getScheduledMaxPower(dateTime));
}
}