diff --git a/CODEOWNERS b/CODEOWNERS index cf6bc84ef17..b556fa80c41 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -319,7 +319,7 @@ /bundles/org.openhab.binding.synopanalyzer/ @clinique /bundles/org.openhab.binding.systeminfo/ @svilenvul /bundles/org.openhab.binding.tacmi/ @twendt @Wolfgang1966 @marvkis -/bundles/org.openhab.binding.tado/ @dfrommi +/bundles/org.openhab.binding.tado/ @dfrommi @andrewfg /bundles/org.openhab.binding.tankerkoenig/ @dolic @JueBag /bundles/org.openhab.binding.tapocontrol/ @wildcs /bundles/org.openhab.binding.telegram/ @ZzetT diff --git a/bundles/org.openhab.binding.tado/README.md b/bundles/org.openhab.binding.tado/README.md index b33aac7a0a8..2f7a70f1669 100644 --- a/bundles/org.openhab.binding.tado/README.md +++ b/bundles/org.openhab.binding.tado/README.md @@ -66,21 +66,23 @@ Name | Type | Description | Read/Write | Zone type -|-|-|-|- `currentTemperature` | Number:Temperature | Current inside temperature | R | `HEATING`, `AC` `humidity` | Number | Current relative inside humidity in percent | R | `HEATING`, `AC` -`heatingPower` | Number | Amount of heating power currently present | R | `HEATING` -`acPower` | Switch | Indicates if the Air-Conditioning is Off or On | R | `AC` `hvacMode` | String | Active mode, one of `OFF`, `HEAT`, `COOL`, `DRY`, `FAN`, `AUTO` | RW | `HEATING` and `DHW` support `OFF` and `HEAT`, `AC` can support more `targetTemperature` | Number:Temperature | Set point | RW | `HEATING`, `AC`, `DHW` +`operationMode` | String | Operation mode the zone is currently in. One of `SCHEDULE` (follow smart schedule), `MANUAL` (override until ended manually), `TIMER` (override for a given time), `UNTIL_CHANGE` (active until next smart schedule block or until AWAY mode becomes active) | RW | `HEATING`, `AC`, `DHW` +`overlayExpiry` | DateTime | End date and time of a timer | R | `HEATING`, `AC`, `DHW` +`timerDuration` | Number | Timer duration in minutes | RW | `HEATING`, `AC`, `DHW` +`heatingPower` | Number | Amount of heating power currently present | R | `HEATING` +`acPower` | Switch | Indicates if the Air-Conditioning is Off or On | R | `AC` `fanspeed`1) | String | Fan speed, one of `AUTO`, `LOW`, `MIDDLE`, `HIGH` | RW | `AC` `fanLevel`1) | String | Fan speed, one of 3) `AUTO`, `SILENT`, `LEVEL1`, `LEVEL2`, `LEVEL3`, `LEVEL4`, `LEVEL5` | RW | `AC` `swing`2) | Switch | Swing on/off | RW | `AC` `verticalSwing`2) | String | Vertical swing state, one of 3) `OFF`, `ON`, `UP`, `MID_UP`, `MID`, `MID_DOWN`, `DOWN`, `AUTO` | RW | `AC` `horizontalSwing`2) | String | Horizontal swing state, one of 3) `OFF`, `ON`, `LEFT`, `MID_LEFT`, `MID`, `MID_RIGHT`, `RIGHT`, `AUTO` | RW | `AC` -`overlayExpiry` | DateTime | End date and time of a timer | R | `HEATING`, `AC`, `DHW` -`timerDuration` | Number | Timer duration in minutes | RW | `HEATING`, `AC`, `DHW` -`operationMode` | String | Operation mode the zone is currently in. One of `SCHEDULE` (follow smart schedule), `MANUAL` (override until ended manually), `TIMER` (override for a given time), `UNTIL_CHANGE` (active until next smart schedule block or until AWAY mode becomes active) | RW | `HEATING`, `AC`, `DHW` -`batteryLowAlarm` | Switch | A control device in the Zone has a low battery (if applicable) | R | Any Zone +`batteryLowAlarm` | Switch | A control device in the Zone has a low battery | R | Any Zone `openWindowDetected` | Switch | An open window has been detected in the Zone | R | Any Zone -`light` | Switch | State (`ON`, `OFF`) of the control panel light (if applicable) | RW | `AC` +`light` | Switch | State (`ON`, `OFF`) of the control panel light | RW | `AC` + +You will see some of the above mentioned Channels only if your tado° device supports the respective function. The `RW` items are used to either override the schedule or to return to it (if `hvacMode` is set to `SCHEDULE`). diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/CapabilitiesSupport.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/CapabilitiesSupport.java new file mode 100644 index 00000000000..d2e5f823c9c --- /dev/null +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/CapabilitiesSupport.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2010-2022 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.tado.internal; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.tado.internal.api.model.AcModeCapabilities; +import org.openhab.binding.tado.internal.api.model.AirConditioningCapabilities; +import org.openhab.binding.tado.internal.api.model.ControlDevice; +import org.openhab.binding.tado.internal.api.model.GenericZoneCapabilities; +import org.openhab.binding.tado.internal.api.model.TadoSystemType; +import org.openhab.binding.tado.internal.api.model.Zone; + +/** + * The {@link CapabilitiesSupport} class checks which type of channels are needed in a thing that is to be built around + * the given capabilities argument, and the (optional) zone argument. It iterates over each of the capabilities + * argument's mode specific sub-capabilities to determine the maximum super set of all sub-capabilities. And it checks + * the capabilities of the optional zone argument too. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class CapabilitiesSupport { + private final TadoSystemType type; + private boolean light; + private boolean swing; + private boolean fanLevel; + private boolean fanSpeed; + private boolean verticalSwing; + private boolean horizontalSwing; + private boolean batteryLowAlarm; + + public CapabilitiesSupport(GenericZoneCapabilities capabilities, Optional zoneOptional) { + type = capabilities.getType(); + + if (zoneOptional.isPresent()) { + Zone zone = zoneOptional.get(); + if (zone.getDevices() != null) { + batteryLowAlarm = zone.getDevices().stream().map(ControlDevice::getBatteryState) + .filter(Objects::nonNull).count() > 0; + } + } + + if (!(capabilities instanceof AirConditioningCapabilities)) { + return; + } + + AirConditioningCapabilities acCapabilities = (AirConditioningCapabilities) capabilities; + + // @formatter:off + Stream<@Nullable AcModeCapabilities> allCapabilities = Stream.of( + acCapabilities.getCOOL(), + acCapabilities.getDRY(), + acCapabilities.getHEAT(), + acCapabilities.getFAN(), + acCapabilities.getAUTO()); + // @formatter:on + + // iterate over all mode capability elements and build the superset of their inner capabilities + allCapabilities.forEach(e -> { + if (e != null) { + light |= e.getLight() != null ? e.getLight().size() > 0 : false; + swing |= e.getSwings() != null ? e.getSwings().size() > 0 : false; + fanLevel |= e.getFanLevel() != null ? e.getFanLevel().size() > 0 : false; + fanSpeed |= e.getFanSpeeds() != null ? e.getFanSpeeds().size() > 0 : false; + verticalSwing |= e.getVerticalSwing() != null ? e.getVerticalSwing().size() > 0 : false; + horizontalSwing |= e.getHorizontalSwing() != null ? e.getHorizontalSwing().size() > 0 : false; + } + }); + } + + public boolean fanLevel() { + return fanLevel; + } + + public boolean fanSpeed() { + return fanSpeed; + } + + public boolean horizontalSwing() { + return horizontalSwing; + } + + public boolean light() { + return light; + } + + public boolean swing() { + return swing; + } + + public boolean verticalSwing() { + return verticalSwing; + } + + public boolean acPower() { + return type == TadoSystemType.AIR_CONDITIONING; + } + + public boolean heatingPower() { + return type == TadoSystemType.HEATING; + } + + public boolean currentTemperature() { + return (type == TadoSystemType.AIR_CONDITIONING) || (type == TadoSystemType.HEATING); + } + + public boolean humidity() { + return (type == TadoSystemType.AIR_CONDITIONING) || (type == TadoSystemType.HEATING); + } + + public boolean batteryLowAlarm() { + return batteryLowAlarm; + } + + public boolean openWindow() { + return (type == TadoSystemType.AIR_CONDITIONING) || (type == TadoSystemType.HEATING); + } +} diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoBatteryChecker.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoBatteryChecker.java index d2edfad2272..23f7fe73f7c 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoBatteryChecker.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoBatteryChecker.java @@ -13,15 +13,17 @@ package org.openhab.binding.tado.internal.handler; import java.io.IOException; -import java.util.Calendar; -import java.util.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.tado.internal.api.ApiException; import org.openhab.binding.tado.internal.api.model.ControlDevice; +import org.openhab.binding.tado.internal.api.model.Zone; import org.openhab.core.library.types.OnOffType; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; @@ -39,43 +41,46 @@ import org.slf4j.LoggerFactory; public class TadoBatteryChecker { private final Logger logger = LoggerFactory.getLogger(TadoBatteryChecker.class); - private final Map zoneList = new HashMap<>(); private final TadoHomeHandler homeHandler; - - private Date refreshTime = new Date(); + private Map zones = new HashMap<>(); + private Instant refreshTime = Instant.MIN; public TadoBatteryChecker(TadoHomeHandler homeHandler) { this.homeHandler = homeHandler; } - private synchronized void refreshZoneList() { - Date now = new Date(); - if (now.after(refreshTime) || zoneList.isEmpty()) { - // be frugal, we only need to refresh the battery state hourly - Calendar calendar = Calendar.getInstance(); - calendar.setTime(now); - calendar.add(Calendar.HOUR, 1); - refreshTime = calendar.getTime(); - - Long homeId = homeHandler.getHomeId(); - if (homeId != null) { - logger.debug("Fetching (battery state) zone list for HomeId {}", homeId); - zoneList.clear(); - try { - homeHandler.getApi().listZones(homeId).forEach(zone -> { - boolean batteryLow = !zone.getDevices().stream().map(ControlDevice::getBatteryState) - .filter(Objects::nonNull).allMatch(s -> s.equals("NORMAL")); - zoneList.put(Long.valueOf(zone.getId()), OnOffType.from(batteryLow)); - }); - } catch (IOException | ApiException e) { - logger.debug("Fetch (battery state) zone list exception"); - } + private void refreshZoneList() { + if (refreshTime.isAfter(Instant.now())) { + return; + } + // only refresh the battery state hourly + refreshTime = Instant.now().plus(1, ChronoUnit.HOURS); + Long homeId = homeHandler.getHomeId(); + if (homeId != null) { + logger.debug("Fetching (battery state) zone list for HomeId {}", homeId); + try { + Map zones = new HashMap<>(); + homeHandler.getApi().listZones(homeId).stream().filter(Objects::nonNull) + .forEach(zone -> zones.put((long) zone.getId(), zone)); + this.zones = zones; + } catch (IOException | ApiException e) { + logger.debug("Fetch (battery state) zone list exception"); } } } - public State getBatteryLowAlarm(long zoneId) { + public synchronized Optional getZone(long zoneId) { refreshZoneList(); - return zoneList.getOrDefault(zoneId, UnDefType.UNDEF); + return Optional.ofNullable(zones.get(zoneId)); + } + + public State getBatteryLowAlarm(long zoneId) { + Optional zone = getZone(zoneId); + if (zone.isPresent()) { + boolean batteryOk = zone.get().getDevices().stream().map(ControlDevice::getBatteryState) + .filter(Objects::nonNull).allMatch(batteryState -> "NORMAL".equals(batteryState)); + return OnOffType.from(!batteryOk); + } + return UnDefType.UNDEF; } } diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java index f5e5bf1b5da..65fa4c076ac 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java @@ -41,8 +41,6 @@ import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,7 +58,7 @@ public class TadoHomeHandler extends BaseBridgeHandler { private final HomeApi api; private @Nullable Long homeId; - private @Nullable TadoBatteryChecker batteryChecker; + private final TadoBatteryChecker batteryChecker; private @Nullable ScheduledFuture initializationFuture; public TadoHomeHandler(Bridge bridge) { @@ -194,8 +192,7 @@ public class TadoHomeHandler extends BaseBridgeHandler { } } - public State getBatteryLowAlarm(long zoneId) { - TadoBatteryChecker batteryChecker = this.batteryChecker; - return batteryChecker != null ? batteryChecker.getBatteryLowAlarm(zoneId) : UnDefType.UNDEF; + public TadoBatteryChecker getBatteryChecker() { + return this.batteryChecker; } } diff --git a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoZoneHandler.java b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoZoneHandler.java index ce30cb37d6e..5d830d76952 100644 --- a/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoZoneHandler.java +++ b/bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoZoneHandler.java @@ -15,7 +15,9 @@ package org.openhab.binding.tado.internal.handler; import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.terminationConditionTemplateToTerminationCondition; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.StringJoiner; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -24,6 +26,7 @@ import javax.measure.quantity.Temperature; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.tado.internal.CapabilitiesSupport; import org.openhab.binding.tado.internal.TadoBindingConstants; import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel; import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing; @@ -281,7 +284,13 @@ public class TadoZoneHandler extends BaseHomeThingHandler { updateProperty(TadoBindingConstants.PROPERTY_ZONE_NAME, zoneDetails.getName()); updateProperty(TadoBindingConstants.PROPERTY_ZONE_TYPE, zoneDetails.getType().name()); + this.capabilities = capabilities; + + CapabilitiesSupport capabilitiesSupport = new CapabilitiesSupport(capabilities, + getHomeHandler().getBatteryChecker().getZone(getZoneId())); + + updateDynamicChannels(capabilitiesSupport); } catch (IOException | ApiException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Could not connect to server due to " + e.getMessage()); @@ -350,7 +359,7 @@ public class TadoZoneHandler extends BaseHomeThingHandler { } updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM, - getHomeHandler().getBatteryLowAlarm(getZoneId())); + getHomeHandler().getBatteryChecker().getBatteryLowAlarm(getZoneId())); } /** @@ -474,4 +483,62 @@ public class TadoZoneHandler extends BaseHomeThingHandler { } return gson.toJson(object); } + + /** + * If the given channel exists in the thing, but is NOT required in the thing, then add it to a list of channels to + * be removed. Or if the channel does NOT exist in the thing, but is required in the thing, then log a warning. + * + * @param removeList the list of channels to be removed from the thing. + * @param channelId the id of the channel to be (eventually) removed. + * @param channelRequired true if the thing requires this channel. + */ + private void removeListProcessChannel(List removeList, String channelId, boolean channelRequired) { + Channel channel = thing.getChannel(channelId); + if (!channelRequired && channel != null) { + removeList.add(channel); + } else if (channelRequired && channel == null) { + logger.warn("Thing {} does not have a '{}' channel => please reinitialize it", thing.getUID(), channelId); + } + } + + /** + * Remove previously statically created channels if the device does not support them. + * + * @param capabilitiesSupport a CapabilitiesSupport instance which summarizes the device's capabilities. + * @throws IllegalStateException if any of the channel builders failed. + */ + private void updateDynamicChannels(CapabilitiesSupport capabilitiesSupport) { + List removeList = new ArrayList<>(); + + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM, + capabilitiesSupport.batteryLowAlarm()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_OPEN_WINDOW_DETECTED, + capabilitiesSupport.openWindow()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_LIGHT, capabilitiesSupport.light()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING, + capabilitiesSupport.horizontalSwing()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING, + capabilitiesSupport.verticalSwing()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_SWING, capabilitiesSupport.swing()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED, + capabilitiesSupport.fanSpeed()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL, + capabilitiesSupport.fanLevel()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_AC_POWER, capabilitiesSupport.acPower()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER, + capabilitiesSupport.heatingPower()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_HUMIDITY, + capabilitiesSupport.humidity()); + removeListProcessChannel(removeList, TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE, + capabilitiesSupport.currentTemperature()); + + if (!removeList.isEmpty()) { + if (logger.isDebugEnabled()) { + StringJoiner joiner = new StringJoiner(", "); + removeList.forEach(c -> joiner.add(c.getUID().getId())); + logger.debug("Removing unsupported channels for {}: {}", thing.getUID(), joiner.toString()); + } + updateThing(editThing().withoutChannels(removeList).build()); + } + } } diff --git a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties index 3cf80d0521b..f79431c95d6 100644 --- a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties +++ b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties @@ -35,8 +35,8 @@ thing-type.config.tado.zone.refreshInterval.description = Refresh interval of ho # channel types -channel-type.tado.acPower.label = AirCon Power State -channel-type.tado.acPower.description = Indicates if the air-conditioning is Off or On +channel-type.tado.acPower.label = Air-conditioning Power +channel-type.tado.acPower.description = Current power state of the air-conditioning channel-type.tado.atHome.label = At Home channel-type.tado.atHome.description = ON if at home, OFF if away channel-type.tado.currentTemperature.label = Temperature diff --git a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml index 58c940d21ba..e1dba5d2f5c 100644 --- a/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml @@ -38,30 +38,24 @@ - - - - + - - + - + + + + - - - + ON if one or more devices in the zone have a low battery - - - @@ -152,6 +146,7 @@ Number Current heating power + Fire @@ -183,6 +178,7 @@ String AC fan speed (only if supported by AC) + Fan @@ -197,18 +193,21 @@ Switch State of AC swing (only if supported by AC) + Flow Switch State of control panel light (only if supported by AC) + Light String AC fan level (only if supported by AC) + Fan @@ -226,6 +225,7 @@ String State of AC horizontal swing (only if supported by AC) + Flow @@ -244,6 +244,7 @@ String State of AC vertical swing (only if supported by AC) + Flow @@ -276,6 +277,7 @@ Number Total duration of a timer + Time @@ -283,6 +285,7 @@ DateTime Time until when the overlay is active. Null if no overlay is set or overlay type is manual. + Time @@ -294,8 +297,9 @@ Switch - - Indicates if the air-conditioning is Off or On + + Current power state of the air-conditioning + Climate @@ -303,7 +307,7 @@ Switch Indicates if an open window has been detected - window + Window diff --git a/bundles/org.openhab.binding.tado/src/test/java/org/openhab/binding/tado/tests/CapabilitiesSupportTest.java b/bundles/org.openhab.binding.tado/src/test/java/org/openhab/binding/tado/tests/CapabilitiesSupportTest.java new file mode 100644 index 00000000000..020d2bb69b3 --- /dev/null +++ b/bundles/org.openhab.binding.tado/src/test/java/org/openhab/binding/tado/tests/CapabilitiesSupportTest.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2010-2022 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.tado.tests; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.tado.internal.CapabilitiesSupport; +import org.openhab.binding.tado.internal.api.model.ACFanLevel; +import org.openhab.binding.tado.internal.api.model.ACVerticalSwing; +import org.openhab.binding.tado.internal.api.model.AcFanSpeed; +import org.openhab.binding.tado.internal.api.model.AcModeCapabilities; +import org.openhab.binding.tado.internal.api.model.AirConditioningCapabilities; +import org.openhab.binding.tado.internal.api.model.ControlDevice; +import org.openhab.binding.tado.internal.api.model.GenericZoneCapabilities; +import org.openhab.binding.tado.internal.api.model.Power; +import org.openhab.binding.tado.internal.api.model.TadoSystemType; +import org.openhab.binding.tado.internal.api.model.Zone; + +import com.google.gson.Gson; + +/** + * The {@link CapabilitiesSupportTest} implements tests of the capabilities support evaluator. + * + * @author Andrew Fiddian-Green - Initial contributions + * + */ +@NonNullByDefault +public class CapabilitiesSupportTest { + + /** + * Test capabilities support (heating) + */ + @Test + void testCapabilitiesSupportHeating() { + GenericZoneCapabilities caps = new GenericZoneCapabilities(); + caps.setType(TadoSystemType.HEATING); + + CapabilitiesSupport capabilitiesSupport = new CapabilitiesSupport(caps, Optional.empty()); + + assertTrue(capabilitiesSupport.heatingPower()); + + assertFalse(capabilitiesSupport.fanLevel()); + assertFalse(capabilitiesSupport.fanSpeed()); + assertFalse(capabilitiesSupport.horizontalSwing()); + assertFalse(capabilitiesSupport.light()); + assertFalse(capabilitiesSupport.swing()); + assertFalse(capabilitiesSupport.verticalSwing()); + assertFalse(capabilitiesSupport.acPower()); + } + + /** + * Test capabilities support (air conditioning) + */ + @Test + void testCapabilitiesSupportAirContitioning() { + AirConditioningCapabilities caps = new AirConditioningCapabilities(); + caps.setType(TadoSystemType.AIR_CONDITIONING); + + AcModeCapabilities heat = new AcModeCapabilities(); + heat.addFanLevelItem(ACFanLevel.LEVEL1); + heat.addSwingsItem(Power.OFF); + caps.HEAT(heat); + + AcModeCapabilities cool = new AcModeCapabilities(); + cool.addFanSpeedsItem(AcFanSpeed.AUTO); + cool.addVerticalSwingItem(ACVerticalSwing.DOWN); + caps.COOL(cool); + + CapabilitiesSupport capabilitiesSupport = new CapabilitiesSupport(caps, Optional.empty()); + + assertTrue(capabilitiesSupport.fanLevel()); + assertTrue(capabilitiesSupport.verticalSwing()); + assertTrue(capabilitiesSupport.acPower()); + assertTrue(capabilitiesSupport.fanSpeed()); + assertTrue(capabilitiesSupport.swing()); + + assertFalse(capabilitiesSupport.horizontalSwing()); + assertFalse(capabilitiesSupport.light()); + assertFalse(capabilitiesSupport.heatingPower()); + } + + /** + * Test capabilities support (battery) + */ + @Test + void testCapabilitiesBattery() { + CapabilitiesSupport capabilitiesSupport; + GenericZoneCapabilities caps = new GenericZoneCapabilities(); + caps.setType(TadoSystemType.HEATING); + + String jsonWithBattery = "{\"deviceType\": \"abc\", \"serialNo\": \"123\", \"batteryState\": \"NORMAL\"}"; + String jsonNoBattery = "{\"deviceType\": \"xyz\", \"serialNo\": \"456\"}"; + + Gson gson = new Gson(); + + Zone zone = new Zone(); + Optional optionalZone = Optional.of(zone); + + // null devices list + capabilitiesSupport = new CapabilitiesSupport(caps, optionalZone); + assertFalse(capabilitiesSupport.batteryLowAlarm()); + + // empty devices list + zone.devices(new ArrayList<>()); + capabilitiesSupport = new CapabilitiesSupport(caps, optionalZone); + assertFalse(capabilitiesSupport.batteryLowAlarm()); + + // list of non battery devices + zone.addDevicesItem(gson.fromJson(jsonNoBattery, ControlDevice.class)); + zone.addDevicesItem(gson.fromJson(jsonNoBattery, ControlDevice.class)); + zone.addDevicesItem(gson.fromJson(jsonNoBattery, ControlDevice.class)); + + capabilitiesSupport = new CapabilitiesSupport(caps, optionalZone); + assertFalse(capabilitiesSupport.batteryLowAlarm()); + + // at least one battery device in list + zone.addDevicesItem(gson.fromJson(jsonWithBattery, ControlDevice.class)); + + capabilitiesSupport = new CapabilitiesSupport(caps, optionalZone); + assertTrue(capabilitiesSupport.batteryLowAlarm()); + + // empty optional + capabilitiesSupport = new CapabilitiesSupport(caps, Optional.empty()); + assertFalse(capabilitiesSupport.batteryLowAlarm()); + } +}