]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tado] Channels are visible depending on device type and capabilities (#13301)
authorAndrew Fiddian-Green <software@whitebear.ch>
Sun, 2 Oct 2022 15:46:16 +0000 (16:46 +0100)
committerGitHub <noreply@github.com>
Sun, 2 Oct 2022 15:46:16 +0000 (17:46 +0200)
* [tado] create zone channels dynamically
* [tado] add JUnit test classes
* [tado] eliminate maven compiler warnings
* [tado] code optimisation
* [tado] read me
* [tado] current temperature & humidity also dynamic
* [tado] battery/window channels
* [tado] simplify channel builder
* [tado] fix bundle initialisation bug
* [tado] fix insert positions
* [tado] add channel type categories
* [tado] refactor battery checker
* [tado] create capabilities support in caller vs callee
* [tado] method doesn't throw exception
* [tado] remove new line
* [velux] add self to code owners
* [tado] capitalize categories

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
CODEOWNERS
bundles/org.openhab.binding.tado/README.md
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/CapabilitiesSupport.java [new file with mode: 0644]
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoBatteryChecker.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoHomeHandler.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoZoneHandler.java
bundles/org.openhab.binding.tado/src/main/resources/OH-INF/i18n/tado.properties
bundles/org.openhab.binding.tado/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.tado/src/test/java/org/openhab/binding/tado/tests/CapabilitiesSupportTest.java [new file with mode: 0644]

index cf6bc84ef17f95a61d215ab7aa905734a1af9f24..b556fa80c4123ced7ab042a9169ffb6ba4c07685 100644 (file)
 /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
index b33aac7a0a809a06135197cb8c8dc3dc7da7a764..2f7a70f166988d4d6b1c58ecfe888b124cbb15b3 100644 (file)
@@ -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`<sup>1)</sup> | String | Fan speed, one of `AUTO`, `LOW`, `MIDDLE`, `HIGH` | RW | `AC`
 `fanLevel`<sup>1)</sup> | String | Fan speed, one of <sup>3)</sup> `AUTO`, `SILENT`, `LEVEL1`, `LEVEL2`, `LEVEL3`, `LEVEL4`, `LEVEL5` | RW | `AC`
 `swing`<sup>2)</sup> | Switch | Swing on/off | RW | `AC`
 `verticalSwing`<sup>2)</sup> | String | Vertical swing state, one of <sup>3)</sup> `OFF`, `ON`, `UP`, `MID_UP`, `MID`, `MID_DOWN`, `DOWN`, `AUTO` | RW | `AC`
 `horizontalSwing`<sup>2)</sup> | String | Horizontal swing state, one of <sup>3)</sup> `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 (file)
index 0000000..d2e5f82
--- /dev/null
@@ -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<Zone> 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);
+    }
+}
index d2edfad22723a584a2074cd1621aef3a63f3ed21..23f7fe73f7cbe77bdd8df1025b0301a7debd6960 100644 (file)
 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<Long, State> zoneList = new HashMap<>();
     private final TadoHomeHandler homeHandler;
-
-    private Date refreshTime = new Date();
+    private Map<Long, Zone> 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<Long, Zone> 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<Zone> getZone(long zoneId) {
         refreshZoneList();
-        return zoneList.getOrDefault(zoneId, UnDefType.UNDEF);
+        return Optional.ofNullable(zones.get(zoneId));
+    }
+
+    public State getBatteryLowAlarm(long zoneId) {
+        Optional<Zone> 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;
     }
 }
index f5e5bf1b5daa86c20adc5938ee4162e914401883..65fa4c076ac1e918124f7a15b775333f8ebacdc7 100644 (file)
@@ -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;
     }
 }
index ce30cb37d6e85eb8ab53ed512ba436800bf9ba2c..5d830d769527e8c169f7de3d42b3e706b7785720 100644 (file)
@@ -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<Channel> 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<Channel> 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());
+        }
+    }
 }
index 3cf80d0521b50ffa8385c862fe5c243341641e11..f79431c95d633be58e1df95624ac8681a1730f37 100644 (file)
@@ -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
index 58c940d21ba3ca8639845505c70fd8389e255405..e1dba5d2f5c7d0b13a4c8df3946612607a894fbb 100644 (file)
                <channels>
                        <channel typeId="currentTemperature" id="currentTemperature"></channel>
                        <channel typeId="humidity" id="humidity"></channel>
-
                        <channel typeId="heatingPower" id="heatingPower"></channel>
-
-                       <channel typeId="hvacMode" id="hvacMode"></channel>
-                       <channel typeId="targetTemperature" id="targetTemperature"></channel>
+                       <channel typeId="acPower" id="acPower"></channel>
                        <channel typeId="fanspeed" id="fanspeed"></channel>
-                       <channel typeId="swing" id="swing"></channel>
-                       <channel typeId="light" id="light"></channel>
                        <channel typeId="fanLevel" id="fanLevel"></channel>
+                       <channel typeId="swing" id="swing"></channel>
                        <channel typeId="horizontalSwing" id="horizontalSwing"></channel>
                        <channel typeId="verticalSwing" id="verticalSwing"></channel>
-
+                       <channel typeId="light" id="light"></channel>
+                       <channel typeId="hvacMode" id="hvacMode"></channel>
+                       <channel typeId="targetTemperature" id="targetTemperature"></channel>
+                       <channel typeId="operationMode" id="operationMode"></channel>
                        <channel typeId="overlayExpiry" id="overlayExpiry"></channel>
                        <channel typeId="timerDuration" id="timerDuration"></channel>
-
-                       <channel typeId="operationMode" id="operationMode"></channel>
-
+                       <channel typeId="openWindowDetected" id="openWindowDetected"></channel>
                        <channel typeId="system.low-battery" id="batteryLowAlarm">
                                <label>Battery Low Alarm</label>
                                <description>ON if one or more devices in the zone have a low battery</description>
                        </channel>
-
-                       <channel typeId="acPower" id="acPower"></channel>
-                       <channel typeId="openWindowDetected" id="openWindowDetected"></channel>
                </channels>
 
                <properties>
                <item-type>Number</item-type>
                <label>Heating Power</label>
                <description>Current heating power</description>
+               <category>Fire</category>
                <state readOnly="true" pattern="%.0f %%"></state>
        </channel-type>
 
                <item-type>String</item-type>
                <label>Fan Speed</label>
                <description>AC fan speed (only if supported by AC)</description>
+               <category>Fan</category>
                <state readOnly="false">
                        <options>
                                <option value="LOW">Low</option>
                <item-type>Switch</item-type>
                <label>Swing</label>
                <description>State of AC swing (only if supported by AC)</description>
+               <category>Flow</category>
        </channel-type>
 
        <channel-type id="light">
                <item-type>Switch</item-type>
                <label>Light</label>
                <description>State of control panel light (only if supported by AC)</description>
+               <category>Light</category>
        </channel-type>
 
        <channel-type id="fanLevel">
                <item-type>String</item-type>
                <label>Fan Speed</label>
                <description>AC fan level (only if supported by AC)</description>
+               <category>Fan</category>
                <state readOnly="false">
                        <options>
                                <option value="SILENT">SILENT</option>
                <item-type>String</item-type>
                <label>Horizontal Swing</label>
                <description>State of AC horizontal swing (only if supported by AC)</description>
+               <category>Flow</category>
                <state readOnly="false">
                        <options>
                                <option value="AUTO">AUTO</option>
                <item-type>String</item-type>
                <label>Vertical Swing</label>
                <description>State of AC vertical swing (only if supported by AC)</description>
+               <category>Flow</category>
                <state readOnly="false">
                        <options>
                                <option value="AUTO">AUTO</option>
                <item-type>Number</item-type>
                <label>Timer Duration</label>
                <description>Total duration of a timer</description>
+               <category>Time</category>
                <state min="0" step="1" pattern="%d min" readOnly="false"></state>
        </channel-type>
 
                <item-type>DateTime</item-type>
                <label>Overlay End Time</label>
                <description>Time until when the overlay is active. Null if no overlay is set or overlay type is manual.</description>
+               <category>Time</category>
                <state readOnly="true" pattern="%1$tF %1$tR"/>
        </channel-type>
 
 
        <channel-type id="acPower">
                <item-type>Switch</item-type>
-               <label>AirCon Power State</label>
-               <description>Indicates if the air-conditioning is Off or On</description>
+               <label>Air-conditioning Power</label>
+               <description>Current power state of the air-conditioning</description>
+               <category>Climate</category>
                <state readOnly="true"></state>
        </channel-type>
 
                <item-type>Switch</item-type>
                <label>Open Window Detected</label>
                <description>Indicates if an open window has been detected</description>
-               <category>window</category>
+               <category>Window</category>
                <state readOnly="true"></state>
        </channel-type>
 
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 (file)
index 0000000..020d2bb
--- /dev/null
@@ -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<Zone> 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());
+    }
+}