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:
| 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:
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" }
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:
Switch item=DaikinACUnit_Zone6 visibility=[DaikinACUnit_Power==ON]
Switch item=DaikinACUnit_Zone7 visibility=[DaikinACUnit_Power==ON]
Switch item=DaikinACUnit_Zone8 visibility=[DaikinACUnit_Power==ON]
-
```
--- /dev/null
+/**
+ * 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) {
+ }
+}
--- /dev/null
+/**
+ * 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));
+ }
+}