[tado] Channels are visible depending on device type and capabilities (#13301)

* [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>
This commit is contained in:
Andrew Fiddian-Green 2022-10-02 16:46:16 +01:00 committed by GitHub
parent 8d9dfb20e3
commit 5c7eaa33ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 411 additions and 62 deletions

View File

@ -319,7 +319,7 @@
/bundles/org.openhab.binding.synopanalyzer/ @clinique /bundles/org.openhab.binding.synopanalyzer/ @clinique
/bundles/org.openhab.binding.systeminfo/ @svilenvul /bundles/org.openhab.binding.systeminfo/ @svilenvul
/bundles/org.openhab.binding.tacmi/ @twendt @Wolfgang1966 @marvkis /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.tankerkoenig/ @dolic @JueBag
/bundles/org.openhab.binding.tapocontrol/ @wildcs /bundles/org.openhab.binding.tapocontrol/ @wildcs
/bundles/org.openhab.binding.telegram/ @ZzetT /bundles/org.openhab.binding.telegram/ @ZzetT

View File

@ -66,21 +66,23 @@ Name | Type | Description | Read/Write | Zone type
-|-|-|-|- -|-|-|-|-
`currentTemperature` | Number:Temperature | Current inside temperature | R | `HEATING`, `AC` `currentTemperature` | Number:Temperature | Current inside temperature | R | `HEATING`, `AC`
`humidity` | Number | Current relative inside humidity in percent | 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 `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` `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` `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` `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` `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` `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` `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` `batteryLowAlarm` | Switch | A control device in the Zone has a low battery | R | Any Zone
`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
`openWindowDetected` | Switch | An open window has been detected in the Zone | 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`). The `RW` items are used to either override the schedule or to return to it (if `hvacMode` is set to `SCHEDULE`).

View File

@ -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);
}
}

View File

@ -13,15 +13,17 @@
package org.openhab.binding.tado.internal.handler; package org.openhab.binding.tado.internal.handler;
import java.io.IOException; import java.io.IOException;
import java.util.Calendar; import java.time.Instant;
import java.util.Date; import java.time.temporal.ChronoUnit;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tado.internal.api.ApiException; 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.ControlDevice;
import org.openhab.binding.tado.internal.api.model.Zone;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
@ -39,43 +41,46 @@ import org.slf4j.LoggerFactory;
public class TadoBatteryChecker { public class TadoBatteryChecker {
private final Logger logger = LoggerFactory.getLogger(TadoBatteryChecker.class); private final Logger logger = LoggerFactory.getLogger(TadoBatteryChecker.class);
private final Map<Long, State> zoneList = new HashMap<>();
private final TadoHomeHandler homeHandler; private final TadoHomeHandler homeHandler;
private Map<Long, Zone> zones = new HashMap<>();
private Date refreshTime = new Date(); private Instant refreshTime = Instant.MIN;
public TadoBatteryChecker(TadoHomeHandler homeHandler) { public TadoBatteryChecker(TadoHomeHandler homeHandler) {
this.homeHandler = homeHandler; this.homeHandler = homeHandler;
} }
private synchronized void refreshZoneList() { private void refreshZoneList() {
Date now = new Date(); if (refreshTime.isAfter(Instant.now())) {
if (now.after(refreshTime) || zoneList.isEmpty()) { return;
// be frugal, we only need to refresh the battery state hourly }
Calendar calendar = Calendar.getInstance(); // only refresh the battery state hourly
calendar.setTime(now); refreshTime = Instant.now().plus(1, ChronoUnit.HOURS);
calendar.add(Calendar.HOUR, 1);
refreshTime = calendar.getTime();
Long homeId = homeHandler.getHomeId(); Long homeId = homeHandler.getHomeId();
if (homeId != null) { if (homeId != null) {
logger.debug("Fetching (battery state) zone list for HomeId {}", homeId); logger.debug("Fetching (battery state) zone list for HomeId {}", homeId);
zoneList.clear();
try { try {
homeHandler.getApi().listZones(homeId).forEach(zone -> { Map<Long, Zone> zones = new HashMap<>();
boolean batteryLow = !zone.getDevices().stream().map(ControlDevice::getBatteryState) homeHandler.getApi().listZones(homeId).stream().filter(Objects::nonNull)
.filter(Objects::nonNull).allMatch(s -> s.equals("NORMAL")); .forEach(zone -> zones.put((long) zone.getId(), zone));
zoneList.put(Long.valueOf(zone.getId()), OnOffType.from(batteryLow)); this.zones = zones;
});
} catch (IOException | ApiException e) { } catch (IOException | ApiException e) {
logger.debug("Fetch (battery state) zone list exception"); logger.debug("Fetch (battery state) zone list exception");
} }
} }
} }
public synchronized Optional<Zone> getZone(long zoneId) {
refreshZoneList();
return Optional.ofNullable(zones.get(zoneId));
} }
public State getBatteryLowAlarm(long zoneId) { public State getBatteryLowAlarm(long zoneId) {
refreshZoneList(); Optional<Zone> zone = getZone(zoneId);
return zoneList.getOrDefault(zoneId, UnDefType.UNDEF); 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;
} }
} }

View File

@ -41,8 +41,6 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -60,7 +58,7 @@ public class TadoHomeHandler extends BaseBridgeHandler {
private final HomeApi api; private final HomeApi api;
private @Nullable Long homeId; private @Nullable Long homeId;
private @Nullable TadoBatteryChecker batteryChecker; private final TadoBatteryChecker batteryChecker;
private @Nullable ScheduledFuture<?> initializationFuture; private @Nullable ScheduledFuture<?> initializationFuture;
public TadoHomeHandler(Bridge bridge) { public TadoHomeHandler(Bridge bridge) {
@ -194,8 +192,7 @@ public class TadoHomeHandler extends BaseBridgeHandler {
} }
} }
public State getBatteryLowAlarm(long zoneId) { public TadoBatteryChecker getBatteryChecker() {
TadoBatteryChecker batteryChecker = this.batteryChecker; return this.batteryChecker;
return batteryChecker != null ? batteryChecker.getBatteryLowAlarm(zoneId) : UnDefType.UNDEF;
} }
} }

View File

@ -15,7 +15,9 @@ package org.openhab.binding.tado.internal.handler;
import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.terminationConditionTemplateToTerminationCondition; import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.terminationConditionTemplateToTerminationCondition;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.StringJoiner;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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;
import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel; import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing; 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_NAME, zoneDetails.getName());
updateProperty(TadoBindingConstants.PROPERTY_ZONE_TYPE, zoneDetails.getType().name()); updateProperty(TadoBindingConstants.PROPERTY_ZONE_TYPE, zoneDetails.getType().name());
this.capabilities = capabilities; this.capabilities = capabilities;
CapabilitiesSupport capabilitiesSupport = new CapabilitiesSupport(capabilities,
getHomeHandler().getBatteryChecker().getZone(getZoneId()));
updateDynamicChannels(capabilitiesSupport);
} catch (IOException | ApiException e) { } catch (IOException | ApiException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Could not connect to server due to " + e.getMessage()); "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, 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); 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());
}
}
} }

View File

@ -35,8 +35,8 @@ thing-type.config.tado.zone.refreshInterval.description = Refresh interval of ho
# channel types # channel types
channel-type.tado.acPower.label = AirCon Power State channel-type.tado.acPower.label = Air-conditioning Power
channel-type.tado.acPower.description = Indicates if the air-conditioning is Off or On channel-type.tado.acPower.description = Current power state of the air-conditioning
channel-type.tado.atHome.label = At Home channel-type.tado.atHome.label = At Home
channel-type.tado.atHome.description = ON if at home, OFF if away channel-type.tado.atHome.description = ON if at home, OFF if away
channel-type.tado.currentTemperature.label = Temperature channel-type.tado.currentTemperature.label = Temperature

View File

@ -38,30 +38,24 @@
<channels> <channels>
<channel typeId="currentTemperature" id="currentTemperature"></channel> <channel typeId="currentTemperature" id="currentTemperature"></channel>
<channel typeId="humidity" id="humidity"></channel> <channel typeId="humidity" id="humidity"></channel>
<channel typeId="heatingPower" id="heatingPower"></channel> <channel typeId="heatingPower" id="heatingPower"></channel>
<channel typeId="acPower" id="acPower"></channel>
<channel typeId="hvacMode" id="hvacMode"></channel>
<channel typeId="targetTemperature" id="targetTemperature"></channel>
<channel typeId="fanspeed" id="fanspeed"></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="fanLevel" id="fanLevel"></channel>
<channel typeId="swing" id="swing"></channel>
<channel typeId="horizontalSwing" id="horizontalSwing"></channel> <channel typeId="horizontalSwing" id="horizontalSwing"></channel>
<channel typeId="verticalSwing" id="verticalSwing"></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="overlayExpiry" id="overlayExpiry"></channel>
<channel typeId="timerDuration" id="timerDuration"></channel> <channel typeId="timerDuration" id="timerDuration"></channel>
<channel typeId="openWindowDetected" id="openWindowDetected"></channel>
<channel typeId="operationMode" id="operationMode"></channel>
<channel typeId="system.low-battery" id="batteryLowAlarm"> <channel typeId="system.low-battery" id="batteryLowAlarm">
<label>Battery Low Alarm</label> <label>Battery Low Alarm</label>
<description>ON if one or more devices in the zone have a low battery</description> <description>ON if one or more devices in the zone have a low battery</description>
</channel> </channel>
<channel typeId="acPower" id="acPower"></channel>
<channel typeId="openWindowDetected" id="openWindowDetected"></channel>
</channels> </channels>
<properties> <properties>
@ -152,6 +146,7 @@
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Heating Power</label> <label>Heating Power</label>
<description>Current heating power</description> <description>Current heating power</description>
<category>Fire</category>
<state readOnly="true" pattern="%.0f %%"></state> <state readOnly="true" pattern="%.0f %%"></state>
</channel-type> </channel-type>
@ -183,6 +178,7 @@
<item-type>String</item-type> <item-type>String</item-type>
<label>Fan Speed</label> <label>Fan Speed</label>
<description>AC fan speed (only if supported by AC)</description> <description>AC fan speed (only if supported by AC)</description>
<category>Fan</category>
<state readOnly="false"> <state readOnly="false">
<options> <options>
<option value="LOW">Low</option> <option value="LOW">Low</option>
@ -197,18 +193,21 @@
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>Swing</label> <label>Swing</label>
<description>State of AC swing (only if supported by AC)</description> <description>State of AC swing (only if supported by AC)</description>
<category>Flow</category>
</channel-type> </channel-type>
<channel-type id="light"> <channel-type id="light">
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>Light</label> <label>Light</label>
<description>State of control panel light (only if supported by AC)</description> <description>State of control panel light (only if supported by AC)</description>
<category>Light</category>
</channel-type> </channel-type>
<channel-type id="fanLevel"> <channel-type id="fanLevel">
<item-type>String</item-type> <item-type>String</item-type>
<label>Fan Speed</label> <label>Fan Speed</label>
<description>AC fan level (only if supported by AC)</description> <description>AC fan level (only if supported by AC)</description>
<category>Fan</category>
<state readOnly="false"> <state readOnly="false">
<options> <options>
<option value="SILENT">SILENT</option> <option value="SILENT">SILENT</option>
@ -226,6 +225,7 @@
<item-type>String</item-type> <item-type>String</item-type>
<label>Horizontal Swing</label> <label>Horizontal Swing</label>
<description>State of AC horizontal swing (only if supported by AC)</description> <description>State of AC horizontal swing (only if supported by AC)</description>
<category>Flow</category>
<state readOnly="false"> <state readOnly="false">
<options> <options>
<option value="AUTO">AUTO</option> <option value="AUTO">AUTO</option>
@ -244,6 +244,7 @@
<item-type>String</item-type> <item-type>String</item-type>
<label>Vertical Swing</label> <label>Vertical Swing</label>
<description>State of AC vertical swing (only if supported by AC)</description> <description>State of AC vertical swing (only if supported by AC)</description>
<category>Flow</category>
<state readOnly="false"> <state readOnly="false">
<options> <options>
<option value="AUTO">AUTO</option> <option value="AUTO">AUTO</option>
@ -276,6 +277,7 @@
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Timer Duration</label> <label>Timer Duration</label>
<description>Total duration of a timer</description> <description>Total duration of a timer</description>
<category>Time</category>
<state min="0" step="1" pattern="%d min" readOnly="false"></state> <state min="0" step="1" pattern="%d min" readOnly="false"></state>
</channel-type> </channel-type>
@ -283,6 +285,7 @@
<item-type>DateTime</item-type> <item-type>DateTime</item-type>
<label>Overlay End Time</label> <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> <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"/> <state readOnly="true" pattern="%1$tF %1$tR"/>
</channel-type> </channel-type>
@ -294,8 +297,9 @@
<channel-type id="acPower"> <channel-type id="acPower">
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>AirCon Power State</label> <label>Air-conditioning Power</label>
<description>Indicates if the air-conditioning is Off or On</description> <description>Current power state of the air-conditioning</description>
<category>Climate</category>
<state readOnly="true"></state> <state readOnly="true"></state>
</channel-type> </channel-type>
@ -303,7 +307,7 @@
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>Open Window Detected</label> <label>Open Window Detected</label>
<description>Indicates if an open window has been detected</description> <description>Indicates if an open window has been detected</description>
<category>window</category> <category>Window</category>
<state readOnly="true"></state> <state readOnly="true"></state>
</channel-type> </channel-type>

View File

@ -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());
}
}