diff --git a/bundles/org.openhab.binding.daikin/README.md b/bundles/org.openhab.binding.daikin/README.md index 1145fb476768d..bca9a6a4ad0d4 100644 --- a/bundles/org.openhab.binding.daikin/README.md +++ b/bundles/org.openhab.binding.daikin/README.md @@ -89,6 +89,9 @@ For the BRP072A42 and BRP072C42: | 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. | +| demandcontrolschedule | A JSON string that contains the scheduled demand control settings. See below. | For the BRP15B61: @@ -110,6 +113,133 @@ 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. +Furthermore, the `demandcontrolmaxpower` channel will provide the _current_ maximum power in effect, as defined within the schedule. +It is important to ensure that openHAB's local time must be in sync with the unit's date/time. +Beware that sending a command to the `demandcontrolmaxpower` will switch the demand control mode to `MANUAL`. + +The schedule can be changed by sending a command to the channel `demandcontrolschedule`. +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": , + "power": + } + ], + "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 +265,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 +286,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 +315,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] - ``` diff --git a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/DaikinBindingConstants.java b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/DaikinBindingConstants.java index a8b54f309be29..d83ff2a75e2d3 100644 --- a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/DaikinBindingConstants.java +++ b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/DaikinBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/DaikinWebTargets.java b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/DaikinWebTargets.java index d8517c3f6ed19..9228d538e8064 100644 --- a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/DaikinWebTargets.java +++ b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/DaikinWebTargets.java @@ -29,6 +29,7 @@ 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 DaikinWebTargets(@Nullable HttpClient httpClient, @Nullable String host, 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 void setStreamerMode(boolean state) throws DaikinCommunicationException { } } + public DemandControl getDemandControl() throws DaikinCommunicationException { + String response = invoke(getDemandControlUri); + return DemandControl.parse(response); + } + + public boolean setDemandControl(DemandControl info) throws DaikinCommunicationException { + Map queryParams = info.getParamString(); + String result = invoke(setDemandControlUri, queryParams); + Map 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); diff --git a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/api/DemandControl.java b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/api/DemandControl.java new file mode 100644 index 0000000000000..280813dc44f91 --- /dev/null +++ b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/api/DemandControl.java @@ -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); + + // create a map of "mo" -> "monday", "tu" -> "tuesday", etc. + private static final List DAYS = List.of("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", + "sunday"); + private static final Map 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> scheduleMap = new HashMap<>(); + + private DemandControl() { + } + + public String getSchedule() { + return gson.toJson(scheduleMap); + } + + public void setSchedule(String schedule) throws JsonSyntaxException { + Map> parsedMap = gson.fromJson(schedule, + new TypeToken>>() { + }.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 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 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 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 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 getParamString() { + Map 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 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) { + } +} diff --git a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/api/Enums.java b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/api/Enums.java index 752c028db8533..3b1918bc80dca 100644 --- a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/api/Enums.java +++ b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/api/Enums.java @@ -209,4 +209,43 @@ public static SpecialMode fromAdvancedMode(AdvancedMode advMode) { 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; + } + } } diff --git a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/handler/DaikinAcUnitHandler.java b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/handler/DaikinAcUnitHandler.java index 21bab3dda4a27..3b34ad5642649 100644 --- a/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/handler/DaikinAcUnitHandler.java +++ b/bundles/org.openhab.binding.daikin/src/main/java/org/openhab/binding/daikin/internal/handler/DaikinAcUnitHandler.java @@ -24,8 +24,10 @@ 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.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.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 autoModeValue = Optional.empty(); + private boolean pollDemandControl = true; + private Optional savedDemandControlSchedule = Optional.empty(); + private Optional savedDemandControlMaxPower = Optional.empty(); public DaikinAcUnitHandler(Thing thing, DaikinDynamicStateDescriptionProvider stateDescriptionProvider, @Nullable HttpClient httpClient) { @@ -153,6 +161,29 @@ protected void pollStatus() throws DaikinCommunicationException { // 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 @@ protected boolean handleCommandInternal(ChannelUID channelUID, Command command) 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 @@ protected void changeStreamer(boolean streamerMode) throws DaikinCommunicationEx 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 * diff --git a/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/i18n/daikin.properties b/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/i18n/daikin.properties index 368bda9c582b0..4c73873687b1c 100644 --- a/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/i18n/daikin.properties +++ b/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/i18n/daikin.properties @@ -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 diff --git a/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/thing/thing-types.xml index c8e6c58e87dc8..64f1a37570d11 100644 --- a/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/thing/thing-types.xml @@ -51,8 +51,14 @@ + + + + + 1 + host @@ -424,6 +430,32 @@ + + String + + The demand control mode + + + + + + + + + + + Dimmer + + The maximum power for demand control in percent. Allowed range is between 40% and 100% in increments of + 5%. + + + + String + + The demand control schedule in JSON format. + + String diff --git a/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/update/update.xml new file mode 100644 index 0000000000000..1185e5614e0e8 --- /dev/null +++ b/bundles/org.openhab.binding.daikin/src/main/resources/OH-INF/update/update.xml @@ -0,0 +1,20 @@ + + + + + + + daikin:acunit-demandcontrolmode + + + daikin:acunit-demandcontrolmaxpower + + + daikin:acunit-demandcontrolschedule + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.daikin/src/test/java/org/openhab/binding/daikin/internal/api/ControlInfoTest.java b/bundles/org.openhab.binding.daikin/src/test/java/org/openhab/binding/daikin/internal/api/ControlInfoTest.java index 25ae882a2a476..2b0cce6a1933d 100644 --- a/bundles/org.openhab.binding.daikin/src/test/java/org/openhab/binding/daikin/internal/api/ControlInfoTest.java +++ b/bundles/org.openhab.binding.daikin/src/test/java/org/openhab/binding/daikin/internal/api/ControlInfoTest.java @@ -22,7 +22,7 @@ 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 * diff --git a/bundles/org.openhab.binding.daikin/src/test/java/org/openhab/binding/daikin/internal/api/DemandControlTest.java b/bundles/org.openhab.binding.daikin/src/test/java/org/openhab/binding/daikin/internal/api/DemandControlTest.java new file mode 100644 index 0000000000000..e213af5c97174 --- /dev/null +++ b/bundles/org.openhab.binding.daikin/src/test/java/org/openhab/binding/daikin/internal/api/DemandControlTest.java @@ -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 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 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> 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 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 expectedParamMap = InfoParser.parse(expectedParamString); + + info.setSchedule(scheduleJson); + + Map paramMap = info.getParamString(); + + expectedParamMap.entrySet().stream().forEach(expectedParam -> assertThat(paramMap, + hasEntry(is(expectedParam.getKey()), is(expectedParam.getValue())))); + } + + private @Nullable Map> parseJson(String json) { + return new Gson().fromJson(json, new TypeToken>>() { + }.getType()); + } + + public static Stream 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)); + } +}