/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
-|-|-|-|-
`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`).
--- /dev/null
+/**
+ * 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);
+ }
+}
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;
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;
}
}
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;
private final HomeApi api;
private @Nullable Long homeId;
- private @Nullable TadoBatteryChecker batteryChecker;
+ private final TadoBatteryChecker batteryChecker;
private @Nullable ScheduledFuture<?> initializationFuture;
public TadoHomeHandler(Bridge bridge) {
}
}
- public State getBatteryLowAlarm(long zoneId) {
- TadoBatteryChecker batteryChecker = this.batteryChecker;
- return batteryChecker != null ? batteryChecker.getBatteryLowAlarm(zoneId) : UnDefType.UNDEF;
+ public TadoBatteryChecker getBatteryChecker() {
+ return this.batteryChecker;
}
}
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;
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;
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());
}
updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM,
- getHomeHandler().getBatteryLowAlarm(getZoneId()));
+ getHomeHandler().getBatteryChecker().getBatteryLowAlarm(getZoneId()));
}
/**
}
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());
+ }
+ }
}
# 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
<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>
--- /dev/null
+/**
+ * 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());
+ }
+}